前端模块化

CommonJS

  • 使用requireexport进行导入导出
  • CommonJSNode的模块化方案,只能在Node端运行,不能在浏览器端,除非使用一些构建工具进行编译(WebpackRollup
  • 特定的第三方库只支持CommonJS,比如下载量很高的ms
  • CommonJS属于动态加载,可以直接require一个变量:require(`./${a}`);
    1
    2
    3
    4
    // sum.js
    exports.sum = (x, y) => x + y;
    // index.js
    const { sum } = require("./sum.js");

ESModule

  • Esmodule 是tc39对于ESMAScript的模块化规范,正因是语言层规范,因此在 Node 及 浏览器中均支持。
    1
    2
    3
    4
    // sum.js
    export const sum = (x, y) => x + y;
    // index.js
    import { sum } from "./sum";
  • 使用importexport进行模块的导入导出
  • Esmodule为静态导入,正因如此,可在编译期进行Tree Shaking,减少 js 体积。
  • 如果需要动态导入,tc39 为动态加载模块定义了 API: import(module)
  • cjs模块输出的是一个值的拷贝,esm输出的是值的引用
  • Node环境下的cjs模块是运行时加载,Webpack环境下的esm模块化是编译时加载
    本质上是多了个文件IO

UMD

一种兼容cjsamd的模块化方案,既可以在node/webpack环境中被require引用,也可以在浏览器中直接用 CDN 被script.src引入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function (root, factory) {
if (typeof define === "function" && define.amd) {
// AMD
define(["jquery"], factory);
} else if (typeof exports === "object") {
// CommonJS
module.exports = factory(require("jquery"));
} else {
// 全局变量
root.returnExports = factory(root.jQuery);
}
})(this, function ($) {
// ...
});

总结

特性 ESM(ES6 模块) UMD(通用模块定义) CJS(CommonJS)
语法 import/export 自执行函数,兼容多种环境 require/exports/module.exports
兼容性 现代浏览器、Node.js(需配置) 所有环境(浏览器、Node.js) Node.js(默认)
加载时机 编译时静态分析(静态结构) 运行时动态加载 运行时动态加载
依赖解析 编译阶段确定依赖关系 运行时判断环境并选择加载方式 运行时执行 require 语句
Tree Shaking 支持 ✅(静态结构可分析) ❌(动态加载) ❌(动态加载)
典型应用场景 现代前端项目(Webpack/Vite) 通用库(需兼容所有环境) Node.js 后端项目
作用域 每个模块有独立作用域 封装在 IIFE 中,避免全局污染 每个文件是独立模块
导出机制 实时绑定(引用共享) 对象拷贝 对象拷贝

常见问题

1、第三方npm包基于commonjs开发,使用到webpack的前端项目中,能否通过esmodule语法import引入

可以通过import引入CommonJS模块,Webpack会自动处理兼容性,比如引入下载量很高的ms

  • 直接导入:在Webpack环境下可直接使用import ms from 'ms'导入,Webpack会自动处理
  • 要注意的是Tree Shaking限制: Webpack只会对Esmodule模块进行TreeShaking
2、如何判断引入的第三方包是否支持 Tree Shaking
  • 只有ESM支持Tree Shaking
  • 要根据第三方依赖的产物来判断(是否打成ESM的格式),而不是根据第三方依赖的源码判断
3、CJS的运行时加载和ESM的编译时加载有什么区别
  • 前端Webpack环境编译时加载模块的执行过程
    源代码(含import语句) → 打包工具(如Webpack) → 分析import依赖 → 生成合并后的代码 → 浏览器直接执行
    关键步骤:

    1. 静态分析:Webpack 在打包时扫描所有 import 语句,构建依赖图。
    2. 代码合并:将所有依赖的模块打包到一个或多个文件中(如 bundle.js)。
    3. 运行时执行:浏览器直接执行打包后的代码,无需再加载其他文件(依赖已包含在主文件中)。
  • Node环境运行时加载模块的执行过程
    代码执行 → 遇到require()语句 → 查找模块文件 → 读取文件内容 → 执行模块代码 → 返回导出值
    关键步骤:

    1. 执行到 require():代码运行到该语句时,触发模块加载逻辑。
    2. 路径解析:根据 require() 的参数(如 ./math.js),查找对应的文件。
    3. 文件读取与执行:读取模块文件内容,创建新的模块上下文并执行代码。
    4. 缓存结果:将模块的 exports 对象存入缓存,下次引用时直接返回缓存。
4、成熟的第三方npm包是怎么去做构建的(以Lodash为例)

Lodash 的源码采用定义全局对象,「无模块化」设计,核心是通过构建工具将函数集合打包为多种格式:

源码组织:所有工具函数(如 map、filter)定义在全局对象上。
构建工具:使用定制的构建系统,将源码转换为:
CJS 格式:lodash/index.js
ESM 格式:lodash-es/index.js
UMD 格式:dist/lodash.js(可通过 CDN 引入)

5、如何把开发的NPM包打成不同的格式(ESM,CJS,UMD)

详见:打包示例
js代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function capitalize(str) {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1);
}

export function truncate(str, maxLength = 10, suffix = '...') {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength) + suffix;
}

export default {
capitalize,
truncate
};

webpack配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

// 公共配置
const commonConfig = {
mode: 'production',
entry: './src/index.js',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
// use: {
// loader: 'babel-loader',
// options: {
// presets: ['@babel/preset-env']
// }
// }
}
]
},
plugins: [
// new CleanWebpackPlugin() // 清理dist目录
],
resolve: {
extensions: ['.js']
}
};

// ESM 配置
const esmConfig = {
...commonConfig,
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'string-utils.esm.js',
library: {
type: 'module' // 输出ESM格式
}
},
experiments: {
outputModule: true // 启用ESM输出
},
optimization: {
minimize: false // 开发环境不压缩
}
};

// CJS 配置
const cjsConfig = {
...commonConfig,
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'string-utils.cjs.js',
library: {
type: 'commonjs2' // 输出CJS格式
}
},
target: 'node', // 针对Node.js环境
optimization: {
minimize: false
}
};

// UMD 配置(开发环境)
const umdDevConfig = {
...commonConfig,
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'string-utils.umd.js',
library: {
name: 'StringUtils', // 全局变量名
type: 'umd' // 输出UMD格式
},
globalObject: 'this' // 兼容浏览器和Node.js
},
optimization: {
minimize: false
}
};

module.exports = [esmConfig, cjsConfig, umdDevConfig, umdProdConfig];