前言
随着 ECMA Script 的发展, 浏览器以及Node.js 对于 ES Module(以下简称 ESM) 的原生支持,不管是在 Node.js 服务端编写接口 APi,还是在浏览器端编写脚本,我们都可以愉快地使用 ES Module, 人们再也不用在模块语法上做来回切换了。 ES Module 语法已经逐渐替换 CommonJS(以下简称 CJS) 成为大家在开发中默认的开发规范,但有时候会疑惑, Webpack 内部是如何兼容不同的模块化文件的,生成的模块化产物又是怎样的,让我们来读一读编译产物
案例分析
此次案例的依赖环境
webpack 版本: 5.50.0
Node.js 版本: 14.5.0
场景模拟
这里设置一个简单的 demo 场景,我们有一个主入口 main.js ,分别加载了一个 CJS 语法文件、ESM 语法文件、还有一个动态加载的 ESM 语法文件。
源文件夹看起来就长这样
main.js
modules/cjs.js
modules/esm.js
async.js
main.js 代码
// main.js
import cjs from './modules/cjs'
import esm, { count, increment } from './modules/esm'
setTimeout(() =>{
import('./async')
},0)
因为一个复杂一点的项目中编译出来的文件都会自动分包,这里模拟一个分包场景,在 webpack.config.js 中做一个 splitChunks 的优化,把 modules 下的文件设置成单独分包。
// webpack.config.js
optimization: {
splitChunks: {
minSize: 0,
chunks: 'all',
cacheGroups: {
modules: {
test: /[\/]modules[\/]/,
priority: 1
},
}
}
}
async.js 因为是动态加载的,所以默认会单独打一个包。
这样通过 Webpack 编译出来产物大概长这样:
index.html
main.bundle.js
modules-src_webpack_modules_cjs_js-src_webpack_modules_esm_js.bundle.js
src_webpack_async_js.js
整体运行流程图
产物分析
先大概解释一下打包产物中的名词解释
module: 项目中使用的每个文件都是一个模块
chunk: 打包规则下多个模块合并成出来的产物
它们的关系是:chunk 包含 module
大致解释一下产物中对象跟方法的用处
webpack__require
webpack 内部 require 模块的实现,通过 webpack_module_cache 实现缓存功能
webpack_modules
所有模块加载完成后的映射,key 为文件路径,value 为编译后的源代码。
类似
{
"./src/mian.js": (module, exports, requrie) => {
...
}
}
其中 webpack__require.m = webpack_modules
webpack__require.O
处理运行时加载的逻辑,第一次执行把传入 undefined 、依赖的 chunk、 当前入口模块的 require 函数, 初始化 deffered 对象之后,执行第二次加载. 执行前面初始化的 deferred 对象中的 fn 方法,初始化入口模块的runtime。
var __webpack_exports__ = __webpack_require__.O(undefined,
["modules-src_webpack_modules_cjs_js-src_webpack_modules_esm_js"],
() => (__webpack_require__("./src/webpack/main.js")))
__webpack_exports__ = __webpack_require__.O(__webpack_exports__);
webpack__require.O.j
判断传入的 chunk 是否已经加载完成
webpack__require.e
执行异步加载的入口逻辑
webpack__require.u
根据 chunkId 拼接 .js 文件后缀
webpack_require.g
取不同环境的下 global 对象,返回 globalThis 对象,默认为 window
webpack__require.o
通过 Object.prototype.hasOwnProperty 查找是否自身属性
webpack__require.l
根据 url、回调、chunk,生成 script 标签请求的逻辑
webpack__require.r
定义为 ES Module 的标志,添加 __esModule 属性以及 Symoble.toStringTag == 'Module' 的逻辑。 Symoble.toStringTag 用于 Object.prototype.toString() 返回的类型
installedChunks
当前 chunk 中包含的 module,默认值是入口 module
webpack__require.f.j
负责异步模块加载中的逻辑,根据 Promise 生成一个 [Promise.resolve, Promise.reject, promise] 的数组形式的加载对象,加载完成后会执行 resolve
在 webpack__require.e 中调用
webpackJsonpCallback
用于更新 installedChunkds 以及 __webpack__modules ,chunk 初始化或者异步代码加载完成后执行
chunkLoadingGlobal
全局对象,通过 SplitChunk 优化或者异步加载分包出来的的 chunk ,加载完成时会 push 到这个数组中
webpack_require.d
给 exports 上的属性定义 getter,用于模拟 es6 中 Living binding 变量
模块
webpack 会把所有模块默认作为 CJS 语法处理,当遇到 import 、export 语法才会调用 webpack__require.r 把它当作 ESM 语法生成。
缓存
通过 webpack_require 时,会先判断缓存对象 webpack_module_cache,没有便会在缓存对象进行初始化,
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
Living Binding
CJS 与 ESM 除了语法上的区别大致有以下:
- ESM 是静态分析,导入的模块不能用变量来进行计算,也就是固定的。
- ESM 中多了一个 default 导出(默认导出)方式,所以我们在使用 require 语法导入 ESM module 时,如果该模块是有默许导出属性,需要额外调用一个 default 属性。eg: require('/xxx').default
- ESM 导出的对象是引用绑定,也就是说当你修改了导入的对象或者方法后,原有的 ESM 模块中的对象或者方法也会相应修改,而 CJS 是值导出。修改导出后的属性不会影响原模块。
Webpack 通过 webpack_require.d 方法来定义 getter 方法,从而使你的修改会应用到原模块上。
/**
* 把 definition 上的值赋给 exports
* @param {*} exports
* @param {*} definition
*/
__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] });
}
}
};
依赖的 chunk 文件
__webpack_require__.d(__webpack_exports__, {
"count": () => (/* binding */ count),
});
var count = 1;
结语
从打包产物看 Webpack 生成的产物逻辑还是比较隐晦的,有些内容比较绕,暂时就先略过了。比如 priority 属性对比的逻辑,循环引用的处理逻辑。待下次可再进行场景的模拟,以及相应的分析。