前端模块化

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包基于CJS开发,使用到Webpack的前端项目中,能否通过ESM语法引入

可以通过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];

6、前端项目用CJS引入模块时,Webpack 会怎么处理?和ESM有何区别?

  • 处理方式:浏览器不原生执行 require / 原始 import 依赖图。Webpack 会把 CJS 与 ESM 都编译成自己的「Webpack 模块」,用运行时里的 __webpack_require__ 按模块 ID 加载、缓存、取导出。
  • 能否打包:静态路径的 require('./a')import 一样可参与依赖分析;动态 require(变量) 依赖不完整,往往需要 require.context 或打成上下文模块,体积与分析能力会变差。
  • 和 ESM 的差异(在 Webpack 语境下)
    • Tree Shaking:ESM 静态 export 更易标未使用导出;纯 CJS 的 exports 对象难证明「未用」,摇树效果通常更弱。
    • 静态分析 / 合并优化:ESM 更利于依赖图完整、作用域提升(如模块串联)等;CJS 动态越多,优化空间越小。

7、Webpack 模块是什么?__webpack_require__ 是什么?

  • Webpack 模块:打包后,每个源文件(经 loader 处理后)通常变成 带数字 ID 的模块工厂函数,放进 modules 数组里;这是 Webpack 自己的模块系统,不是 Node CJS、也不是浏览器原生 ESM。
  • __webpack_require__(moduleId):打进 bundle 的 模块加载器,负责按 ID 执行模块函数、写入 module.exports / 导出表、并 缓存,保证同一 ID 只执行一次(类似单例)。
  • 整体形态:常见是在 最外层 IIFE 里挂载 modules + __webpack_require__,入口再 __webpack_require__(0) 启动。

示意(真实产物经压缩后更长,结构类似):

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
// 最外层 IIFE:立即执行,形参 modules 就是下面的「模块数组」,避免污染全局
(function (modules) {
// 已加载模块缓存:id → { exports },同一 id 只执行一次工厂函数
var installed = {};

// Webpack 运行时核心:按数字 id 加载模块
function __webpack_require__(id) {
// 已加载过:直接返回上次挂好的 exports(单例)
if (installed[id]) return installed[id].exports;

// 新建模块对象,先放进缓存,再执行工厂函数(避免循环依赖时死循环的细节这里略)
var module = (installed[id] = { exports: {} });

// 执行 modules[id] 这个「包了一整个源文件」的函数:
// 传入 module、module.exports、以及 __webpack_require__ 方便模块里再 require 别人
modules[id](module, module.exports, __webpack_require__);

// CJS 风格:模块通过改写 module.exports / exports 对外暴露
return module.exports;
}

// 从入口模块 id(一般是 0)启动整棵依赖树
return __webpack_require__(0);
})([
/* ========== 模块 0:入口 entry.js ==========
* 源码类似:import { sum } from './sum'
* 编译后变成:从 id 为 1 的模块取 exports */
function (module, exports, __webpack_require__) {
const { sum } = __webpack_require__(1);
console.log(sum(1, 2));
},

/* ========== 模块 1:sum.js ==========
* 源码类似:export const sum = (a,b)=>...
* 编译后往 exports 上挂属性 */
function (module, exports, __webpack_require__) {
exports.sum = (a, b) => a + b;
},
]);