背景
文中我们详细讲解了Webpack默认的通用打包规则以及seal阶段的一些执行逻辑。 下面我们继续根据Webpack的执行流程来深入分析其实现原理。 具体内容包括:
事实上,这篇文章以及后面的一些原则性的文章可能并不能立即解决你在业务中可能面临的实际问题,但在更长的时间维度上,那些文章中所呈现的知识、思考和思辨过程可能可以解决你的问题。从长远来看,还可以为您提供:
因此,希望感兴趣的朋友能够坚持下去,以后我会输出很多关于Webpack实现原理的文章! 如果你只是想提高自己在Webpack方面的知识储备,那就关注我,我们一起学习吧!
编译产品分析
为了业务项目能够正常正确的运行,Webpack需要将开发者编写的业务代码以及支持并部署此业务代码的**“运行时”**打包到产品(bundle)中。 用建筑来比喻webpack按需引入,业务代码就相当于一砖一瓦,是看得见、摸得着、直接感知的逻辑; 运行时间相当于埋在砖瓦下的加固地基,一般被忽视,但却决定了整个建筑的功能和质量。 大多数 Webpack 功能都需要特定的 Reinforcement Foundation 才能运行,例如:
我们先从最简单的例子开始,逐步展开了解各个特性下的Webpack运行时代码。
基本结构
我们先从最简单的例子开始,代码结构如下:
// a.js
export default 'a module';
// index.js
import name from './a'
console.log(name)
使用以下配置:
module.exports = {
entry: "./src/index",
mode: "development",
devtool: false,
output: {
filename: "[name].js",
path: path.join(__dirname, "./dist"),
},
};
配置的内容比较简单,就不展开了,直接看编译生成的结果:
图片
虽然看起来很非主流,但仔细分析还是可以反汇编代码上下文的。 该bundle整体被一个IIFE包裹,上面的内容从上到下如下:
这些__webpack_开头的奇怪函数可以统称为Webpack运行时代码。 其作用就是搭建上面提到的整个业务项目的骨架。 上面简单示例中列出的函数和对象 而言,它们协作建立一个简单的模块化系统,以实现 ESModule 规范所声明的模块化特性。 上例中最后一个函数是__webpack_require__,它实现了模块间引用功能,核心代码:
function __webpack_require__(moduleId) {
/******/ // 如果模块被引用过
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/
}
/******/ // Create a new module (and put it into the cache)
/******/ var module = (__webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {},
/******/
});
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](
module,
module.exports,
__webpack_require__
);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/
}
从代码可以推断其作用:
其中,业务模块代码存储在bundle开头的__webpack_modules__变量中,内容如下:
var __webpack_modules__ = {
"./src/a.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
// ...
},
};
结合 __webpack_require__ 函数和 __webpack_modules__ 变量可以正确引用代码模块,例如上例中生成代码末尾的 IIFE:
(() => {
/*!**********************!*
!*** ./src/index.js ***!
**********************/
/* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(/*! ./a */ "./src/a.js");
console.log(_a__WEBPACK_IMPORTED_MODULE_0__.name);
})();
这些函数和对象构成了Webpack运行时最基本的能力——模块化。 我们将在《实现原理》文章的第二节中讨论它们的生成规则和原理。 接下来我们继续看一下异步模块加载和模块。 热更新场景下对应的运行时内容。
异步模块加载
我们来看一个简单的异步模块加载示例:
// ./src/a.js
export default "module-a"
// ./src/index.js
import('./a').then(console.log)
webpack 配置与上面的示例类似:
module.exports = {
entry: "./src/index",
mode: "development",
devtool: false,
output: {
filename: "[name].js",
path: path.join(__dirname, "./dist"),
},
};
生成的代码太长,就不贴出来了。 与最初的基本结构示例中所示的模块化功能相比,使用异步模块加载功能时,将额外减少以下运行时间:
建议读者运行示例来对比实际生成的代码,体验其具体功能。 这些运行时模块建立了Webpack的异步加载能力,其核心是__webpack_require__.e函数,其代码非常简单:
__webpack_require__.f = {};
/******/ // This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = (chunkId) => {
/******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
/******/ __webpack_require__.f[key](chunkId, promises);
/******/ return promises;
/******/ }, []));
/******/ };
从代码来看webpack按需引入,它只是基于__webpack_require__.f实现了一种中间件模式,并使用Promise.all实现并行处理。 实际的加载工作是通过__webpack_require__.fj和__webpack_require__.l来实现的。 分别看两个函数:
/******/ __webpack_require__.f.j = (chunkId, promises) => {
/******/ // JSONP chunk loading for javascript
/******/ var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
/******/ if(installedChunkData !== 0) { // 0 means "already installed".
/******/
/******/ // a Promise means "currently loading".
/******/ if(installedChunkData) {
/******/ promises.push(installedChunkData[2]);
/******/ } else {
/******/ if(true) { // all chunks have JS
/******/ // ...
/******/ // start chunk loading
/******/ var url = __webpack_require__.p + __webpack_require__.u(chunkId);
/******/ // create error before stack unwound to get useful stacktrace later
/******/ var error = new Error();
/******/ var loadingEnded = ...;
/******/ __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
/******/ } else installedChunks[chunkId] = 0;
/******/ }
/******/ }
/******/ };
__webpack_require__.fj实现了异步chunk路径的拼接、缓存和异常处理的逻辑,__webpack_require__.l函数:
/******/ var inProgress = {};
/******/ // data-webpack is not used as build has no uniqueName
/******/ // loadScript function to load a script via script tag
/******/ __webpack_require__.l = (url, done, key, chunkId) => {
/******/ if(inProgress[url]) { inProgress[url].push(done); return; }
/******/ var script, needAttach;
/******/ if(key !== undefined) {
/******/ var scripts = document.getElementsByTagName("script");
/******/ // ...
/******/ }
/******/ // ...
/******/ inProgress[url] = [done];
/******/ var onScriptComplete = (prev, event) => {
/******/ // ...
/******/ }
/******/ ;
/******/ var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
/******/ script.onerror = onScriptComplete.bind(null, script.onerror);
/******/ script.onload = onScriptComplete.bind(null, script.onload);
/******/ needAttach && document.head.appendChild(script);
/******/ };
__webpack_require__.l 通过脚本实现异步chunk内容的加载和执行。 e+l+fj 三个运行时函数支持 Webpack 运行异步模块的能力。 实际使用时,只需要调用e函数即可完成异步模块的加载和运行。 例如上例中对应生成的入口内容:
/*!**********************!*
!*** ./src/index.js ***!
**********************/
__webpack_require__.e(/*! import() */ "src_a_js").then(__webpack_require__.bind(__webpack_require__, /*! ./a */ "./src/a.js"))
模块热更新
模块热更新——HMR是一项可以显着提升开发效率的能力。 它还可以单独编译模块,并在模块代码发生变化时将最新的编译结果发送到浏览器。 然后浏览器使用新的模块代码替换旧的代码,从而实现模块级代码热替换能力。 说到最终的体验,开发者启动Webpack后,在编译和修改代码的过程中不需要自动刷新浏览器页面,所有的变化都可以实时、同步地显示在页面上。 从实现上来说,HMR的实现环节非常漫长,也很有趣。 我们稍后将在另一篇文章中讨论它。 本文主要关注HMR特性带来的运行时代码。 启用HMR功能需要一些特殊的配置项:
module.exports = {
entry: "./src/index",
mode: "development",
devtool: false,
output: {
filename: "[name].js",
path: path.join(__dirname, "./dist"),
},
// 简单起见,这里使用 HtmlWebpackPlugin 插件自动生成作为 host 的 html 文件
plugins: [
new HtmlWebpackPlugin({
title: "Hot Module Replacement",
}),
],
// 配置 devServer 属性,启动 HMR
devServer: {
contentBase: "./dist",
hot: true,
writeToDisk: true,
},
根据上面的配置,使用命令 webpackserve--hot-only 启动 Webpack,可以在 dist 文件夹中找到该产品:
图片
对比上面两个例子,HMR形成的运行时代码达到了1.5w+行,可谓炸裂。 主要运行时内容为:
可以看出,HMR运行时是前面的异步模块加载运行时的超集,异步模块加载运行时是第一个基本示例运行时的超集,层层叠加。 包含在 HMR 中:
内容太多了,所以我们上次开了一篇文章来谈谈HMR。
实现原理
仔细看完上面三个例子,相信读者应该已经隐约捕捉到了一些重要的规律:
在Webpack源码实现中,运行时生成逻辑可以定义为两个步骤:
“依赖收集”:遍历业务代码模块,收集模块的特征依赖关系,进而确定整个项目在Webpackruntime上的依赖列表
“生成”:合并运行时的依赖列表并将其打包到最终的输出包中
这两个步骤都发生在打包阶段,即 Webpack(v5)源代码的compilation.seal 函数中:
图片
上图是我总结的Webpack知识图谱的一部分。 您可以关注公众号【Tecvan】并回复【1】获取在线地址
注意上图,当进入运行时处理环节时,Webpack已经分析出了ModuleDependencyGraph和ChunkGraph的关系,这意味着此时已经可以估计出来了:
对于bundle、module、chunk关系概念不清楚的朋友,建议扩展阅读:
基于此信息,需要首先收集运行时依赖项。
依赖集合
Webpack运行时的依赖在概念上和Vue的依赖类似,都是用来表达模块与其他模块之间的从属关系,但是从实现上来说,Vue是基于运行过程中的动态收集,而Webpack则是基于静态代码分析。 实现逻辑大致如下:
图片
运行时依赖的估计逻辑集中在compilation.processRuntimeRequirements函数中,代码中包含三个循环:
让我们扩展下面的细节。
第一个周期:收集模块依赖关系
在打包(密封)阶段,完成ChunkGraph的建立后,Webpack会调用codeGeneration函数遍历模块字段,并调用其module.codeGeneration函数进行模块翻译。 模块翻译结果如下:
图片
其中,sources属性是模块翻译后的结果; 而runtimeRequirements是根据AST估计的。 这是运行模块所需的运行时间。 估算过程与本文主题无关。 下次我们就挖个洞。 一直在说话。 所有模块翻译完成后,开始调用compilation.processRuntimeRequirements进入第一个循环,将上述翻译结果的runtimeRequirements记录到ChunkGraph对象中。
第二个周期:整合块依赖关系
第一个循环收集模块的依赖关系,第二个循环遍历 chunk 字段收集所有模块对应的运行时依赖关系,例如:
图片
在示例图中,modulea 包含两个运行时依赖项; moduleb 包含一个运行时依赖项。 经过第二轮集成后,相应的 chunk 将包含与两个模块对应的三个运行时依赖项。
第三个周期:依赖标志传递给RuntimeModule对象
源码中,第三个循环代码最少,但逻辑最复杂,大致执行了三个操作:
至此,运行时依赖已经完成了从模块内容分析,到收集,到创建对应的Module泛型类型,再到将Module添加到ModuleDepedencyGraph/ChunkGraph系统中的全过程。 业务代码和运行时代码对应的模块依赖图已经完全准备好,可以计划进入下一阶段——生成最终产品。 但在继续解释产品逻辑之前,我们需要解决两个问题:
总结:Chunk 和 RuntimeChunk
在这篇文章中,我尝试全面解释 Webpack 的默认传递规则,并回顾在三种特定情况下,Webpack 会创建新的 chunk:
默认情况下,initialchunk 一般包含了运行条目所需的所有运行时代码,但 webpack5 后出现的第三条规则打破了这一限制,允许开发者将运行时从initialchunk 中分离出来,成为多个条目之间共享的runtimechunk。 同样,异步模块对应的大部分运行时代码都包含在相应的referrer header中,例如:
// a.js
export default 'a-module'
// index.js
// 异步引入 a 模块
import('./a').then(console.log)
本例中,index异步导入模块a,因此根据默认的分配规则,会形成两个chunk:入口文件index对应的初始chunk,以及异步模块a对应的async chunk。 此时,从ChunkGraph的角度来看,chunk[index]是chunk[a]的父级,代码在运行时会被黑入chunk[index]。 从浏览器的角度来看,在运行 chunk[a] 之前必须先运行 chunk[a]。 索引],两人有显着的亲子关系。
摘要:RuntimeModule系统
当我第一次读Webpack源码时,感觉很奇怪。 Module是Webpack资源管理的基本单位,但Module总共派生了54个子类,其中大部分都是Module=>RuntimeModule=>xxxRuntimeModule关系的继承:
图片
文章中我们讲了模块依赖图的生成过程和作用,但是文章内容主要围绕业务代码展开,大部分使用的是NormalModule。 当seal函数收集运行时时,RuntimePlugin会为运行时依赖一一创建对应的RuntimeModule泛型,例如:
因此,可以推断,所有RuntimeModule末尾的类型都与具体的运行时函数一一对应。 收集依赖项的结果是在业务代码之外创建一堆支持性的 RuntimeModule 泛型。 然后这个通用对象被添加到 ModuleDependencyGraph 中。 它包含在整个模块依赖系统中。
资源合并生成
经过前面的运行时依赖收集过程,bundle需要的所有内容都准备好了,接下来就可以规划写入文件了,这就是右侧核心流程中的生成(emit)阶段:
图片
我的另一篇文章对这部分有更详细的解释。 这里从运行时的角度简单说一下代码流程:
挖一个洞
Webpack 确实很复杂。 每次你自信地写出一个主题的内容时,你都会发现更多新的陷阱,比如从这篇文章中可以衍生出的担忧:
慢慢挖坑,慢慢填坑。
安利抖音电商技术沙龙
5.30我带大家走进抖音电商,了解抖音电商平台架构的技术变化、电商推广的高并发场景、高可用性能的优化、技术的成长FE在电商业务上的应用,以及抖音的跨平台开发。 端到端的动态进化,全方位的技术干货和高能量输出,等你来看看!
时间:5月30日14:00-18:30
地点:北京市松江区亿达路2000号丽丰广场1号楼
线下活动报名:
在线直播用户无需进群! 直播时会再次发送直播链接~
直播链接:
添加机器人回复“抖音电商”进群~~~