尽管现在主流浏览器已支持ESM,但是webpack仍然会将ESM转化为CommonJS。我们来看下 webpack 是 如何将ESM转化为CommonJS的。
CJS
再看ESM打包产物之前,我们先看下CJS的打包产物。
CJS 示例代码
入口文件index.js与sum.js内容如下👇
// index.js
const sum = require('./sum')
console.log(sum(1, 2))
// sum.js
module.exports = (...args) => args.reduce((x, y) => x + y, 0)
webpack打包文件build.js内容如下👇
const webpack = require('webpack')
function f1() {
return webpack({
entry: './index.js',
mode: 'none',
})
}
f1().run((err, stat) => {
if (err) throw err
console.log('🚀 构建完成')
})
运行 node 命令
node index.js
CJS 产物分析
打包完成以后,目录下成功输出dist/main.js,内容如下👇
(() => { // webpackBootstrap
// 模块存放在这个数组中
var __webpack_modules__ = ([
/* 0 */, // 相当于是index.js
((module) => { // 模块1 存放的就是 sum 模块
module.exports = (...args) => args.reduce((x, y) => x + y, 0)
})
]);
// 模块缓存对象
var __webpack_module_cache__ = {};
// 模块加载函数
function __webpack_require__(moduleId /* 模块id */) {
// 检查模块是否已经在缓存中
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 不在缓存中则创建一个模块对象,并将其放入到缓存中
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
// 执行这个要加载到模块函数
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// 返回导出的 exports 对象(当然不一定就是个对象,如果模块中将 module.exports = 1,那么就是返回 1)
return module.exports;
}
var __webpack_exports__ = {};
(() => {
// 通过 __webpack_require__ 函数加载 模块 1(sum模块)
const sum = __webpack_require__(1)
console.log(sum(1, 2))
})();
})()
解析打包后到内容如下👇:
- 模块函数数组 webpack_modules:用于存放模块内容的数组,其数组下标表示对应的模块函数。
- 模块缓存对象 webpack_module_cache:通过模块函数数组的下标缓存模块的对象。
- 模块加载函数 webpack_require:该函数接收一个模块id(对应的就是模块函数数组的下标),如果该模块在缓存中存在则直接返回module.exports。否则将要加载的模块缓存到模块缓存对象 webpack_module_cache 中,并返回module.exports。
最后调用 webpack_require(moduleId) 就可以拿到module.exports的内容了,我们这个地方就是sum函数了
ESM
ESM示例代码
入口文件index.js与sum.js内容如下👇
// index.js
import sum, { name } from './sum'
import * as s from './sum'
console.log(sum(3, 4))
console.log(name)
console.log(s)
// sum.js
const sum = (...args) => args.reduce((x, y) => x + y, 0)
export default sum
export const name = 'sum'
webpack打包文件build.js内容如下👇
const webpack = require('webpack')
function fn() {
return webpack({
entry: './index.js',
mode: 'none'
})
}
fn().run((err, stat) => {
if (err) throw err
console.log('🚀 构建完成')
})
ESM 产物分析
执行node build.js打包ESM得到的产物相比较于CJS而言,在__webpack_require__上多了如下几个属性👇
__webpack_require__.d方法在模块加载时通过Object.defineProperty的getter定义exports导出的值。__webpack_require__.o是Object.prototype.hasOwnProperty的缩写。__webpack_require__.r用来标记是ESM。
(() => {
// 使用 getter 用以定义 exports 的属性
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();
(() => {
// Object.prototype.hasOwnProperty 的简写
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
(() => {
// exports.__esModule = true,用以标记一个 ESM 模块
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
我们写的的ESM代码被打包出来的内容(包含__webpack_require__)如下👇
(() => { // webpackBootstrap
"use strict";
var __webpack_modules__ = ([
/* 0 */,
/* 1 */
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// 标记是 ESM
__webpack_require__.r(__webpack_exports__);
// 设置模块 exports 值
__webpack_require__.d(__webpack_exports__, {
// 注意这里的值是个函数,会跟 __webpack_require__.d 方法中的 Object.defineProperty 的 getter 配合使用
"default": () => (__WEBPACK_DEFAULT_EXPORT__),
"name": () => (/* binding */ name)
});
const sum = (...args) => args.reduce((x, y) => x + y, 0)
// 常量__WEBPACK_DEFAULT_EXPORT__标记默认导出 export default
const __WEBPACK_DEFAULT_EXPORT__ = (sum);
const name = 'sum'
})
]);
// 模块缓存
var __webpack_module_cache__ = {};
// require
function __webpack_require__(moduleId) {
// 检查模块是否在缓存
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 创建一个新模块,并将其放入到缓存中
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
// 执行模块函数
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// 返回模块
return module.exports;
}
(() => {
__webpack_require__.r(__webpack_exports__);
// 加载模块,并返回模块内容
var _sum__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
console.log((0, _sum__WEBPACK_IMPORTED_MODULE_0__["default"])(3, 4))
console.log(_sum__WEBPACK_IMPORTED_MODULE_0__.name)
console.log(_sum__WEBPACK_IMPORTED_MODULE_0__)
})();
})();
可以看到,在 ESM 中有两种导出方式,所以webpack在将ESM转化为CJS时会将export default转化为module.exports.default,将export name这种方式转化为module.exports的属性即可。
在 CJS 中本质上只有一种导出方式——导出的结果都在
module.exports上。 对 CJS 与 ESM 不了解的,可以看这篇👉 CJS与ESM