示例代码
假设我们有两个模块:一个 ESM 格式和一个 CommonJS 格式:
-
math.js
(CommonJS)javascript// math.js module.exports = { add: (a, b) => a + b, multiply: (a, b) => a * b, };
-
index.js
(ESM)javascript// index.js import { add } from './math.js'; console.log(add(2, 3));
使用 Webpack 编译后的代码
Webpack 将上述文件打包后会生成一个闭包包裹的 bundle.js
文件,以下是关键部分的结构和解释:
javascript
// Webpack 编译后的代码
(function(modules) {
// 模拟 CommonJS 的模块加载函数
function __webpack_require__(moduleId) {
var module = { exports: {} };
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports;
}
// 模块定义
var modules = {
'./math.js': function(module, exports) {
// 将 CommonJS 的 module.exports 暴露给外部
module.exports = {
add: (a, b) => a + b,
multiply: (a, b) => a * b,
};
},
'./index.js': function(module, __webpack_exports__, __webpack_require__) {
// ESM 导入的模拟:使用 CommonJS 加载模块
const mathModule = __webpack_require__('./math.js');
// 将 mathModule 解构并作为 ESM 的导出
const { add } = mathModule;
// 执行代码
console.log(add(2, 3));
}
};
// 启动应用
__webpack_require__('./index.js');
})(modules);
Webpack 处理流程详解
-
模块闭包和
__webpack_require__
函数:Webpack 将所有模块打包在
modules
对象中,并通过__webpack_require__
函数来实现模块加载。在这里,modules
是一个对象,键是模块路径,值是模块代码的函数封装。 -
CommonJS 模块处理:
- 在
./math.js
中,Webpack 将 CommonJS 模块的内容直接作为module.exports
挂载到exports
上。 module.exports
是 CommonJS 的导出机制,Webpack 并没有改变其结构,确保其在模块被require
时能够返回正确的对象。
- 在
-
ESM 模块处理与兼容性:
在 ESM 模块
./index.js
中,Webpack 采用了以下策略来处理import { add } from './math.js'
:__webpack_require__('./math.js')
:通过__webpack_require__
加载./math.js
,返回的mathModule
是module.exports
对象。const { add } = mathModule
:通过对象解构语法来模拟 ESM 的导入。Webpack 将 CommonJS 的module.exports
转换为 ESM 导入结构,使add
可以直接使用。
-
兼容性支持:
在实际打包中,Webpack 会添加更多的辅助代码来确保 CommonJS 和 ESM 之间的兼容性。特别是对于 ESM 导入 CommonJS 的情况,Webpack 会检测
module.exports
是否具有__esModule
标志,以确保模块在 ESM 语法中能够正确访问。Webpack 实际上会将 CommonJS 模块包装为 ESM 的
default
导出,确保代码的 ESM 导入方式能够使用import mathModule from './math.js'
直接访问整个模块内容。示例如下:javascript// Webpack 兼容性辅助函数(简化) function __webpack_interop_require_default(module) { return module && module.__esModule ? module : { default: module }; }
这个辅助函数在加载 CommonJS 模块时,检查模块是否带有
__esModule
标志。如果没有,它会将整个module.exports
对象作为default
属性进行包装,使其符合 ESM 的导入结构。 -
Tree Shaking 支持:
对于 ESM 模块的静态导入,Webpack 能够在编译时确定哪些导出被使用,实现 Tree Shaking。例如,如果
index.js
只导入了add
而没有使用multiply
,Webpack 可以优化打包,移除未使用的导出代码。对于 CommonJS,因为其动态性,Webpack 无法静态分析依赖关系,通常会将整个模块内容打包进来。
额外场景:动态导入
如果我们在 index.js
中使用动态导入:
javascript
// index.js
import('./math.js').then(math => {
console.log(math.add(2, 3));
});
Webpack 会检测到 import()
,并生成一个新的代码块 (chunk),将 math.js
模块放入该块中,实现按需加载。
编译后的关键代码示例如下:
javascript
// 动态导入的处理
__webpack_require__.e('./math.js').then(__webpack_require__.bind(__webpack_require__, './math.js'));
在这个代码中:
__webpack_require__.e
:这是一个异步加载模块的函数。Webpack 会将math.js
打包成一个独立的块,并且在需要时加载这个块,加载后再调用__webpack_require__
获取导出内容。- 返回
Promise
:动态导入返回Promise
,当模块加载完成后解析并返回模块内容。
总结
Webpack 在打包 CommonJS 和 ESM 时主要采用以下策略:
- CommonJS :直接将
module.exports
保留在打包中,通过__webpack_require__
加载时返回exports
对象。 - ESM :通过
__webpack_require__
和对象解构的方式模拟 ESM 的import
导入,并利用辅助代码处理 CommonJS 的默认导出以确保兼容。 - Tree Shaking:ESM 的静态导入支持 Tree Shaking,而 CommonJS 因为其动态特性一般无法被 Tree Shaking 优化。
- 动态导入 :Webpack 将
import()
识别为分割点,生成独立的代码块,在需要时异步加载,返回Promise
。