You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
这几个 __webpack_ 开头奇奇怪怪的函数可以统称为 Webpack 运行时代码,作用如前面所说的是搭起整个业务项目的骨架,就上述简单示例所罗列出来的几个函数、对象而言,它们协作构建起一个简单的模块化体系从而实现 ES Module 规范所声明的模块化特性。
上述示例中最终的函数是 __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; /******/ }
在上一篇文章 有点难的 webpack 知识点:Chunk 分包规则详解 中,我们详细讲解了 Webpack 默认的分包规则,以及一部分 seal 阶段的执行逻辑,现在我们将按 Webpack 的执行流程,继续往下深度分析实现原理,具体内容包括:
bundle
?实际上,本文及前面几篇原理性质的文章,可能并不能马上解决你在业务中可能正在面临的现实问题,但放到更长的时间维度,这些文章所呈现的知识、思维、思辨过程可能能够长远地给到你:
所以,希望感兴趣的同学能够坚持,我后续还会输出很多关于 Webpack 实现原理的文章!如果你恰好也想提升自己在 Webpack 方面的知识储备,关注我,我们一起学习!
为了正常、正确运行业务项目,Webpack 需要将开发者编写的业务代码以及支撑、调配这些业务代码的**「运行时」**一并打包到产物 (bundle) 中,以建筑作类比的话,业务代码相当于砖瓦水泥,是看得见摸得着能直接感知的逻辑;运行时相当于掩埋在砖瓦之下的钢筋地基,通常不会关注但决定了整座建筑的功能、质量。
大多数 Webpack 特性都需要特定钢筋地基才能跑起来,比如说:
下面先从最简单的示例开始,逐步展开了解各个特性下的 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_modules__
对象,包含了除入口外的所有模块,示例中即a.js
模块__webpack_module_cache__
对象,用于存储被引用过的模块__webpack_require__
函数,实现模块引用 (require) 逻辑__webpack_require__.d
,工具函数,实现将模块导出的内容附加的模块对象上__webpack_require__.o
,工具函数,判断对象属性用__webpack_require__.r
,工具函数,在 ESM 模式下声明 ESM 模块标识index.js
,用于启动整个应用这几个
__webpack_
开头奇奇怪怪的函数可以统称为 Webpack 运行时代码,作用如前面所说的是搭起整个业务项目的骨架,就上述简单示例所罗列出来的几个函数、对象而言,它们协作构建起一个简单的模块化体系从而实现 ES Module 规范所声明的模块化特性。上述示例中最终的函数是
__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; /******/ }
从代码可以推测出,它的功能:
moduleId
参数找到对应的模块代码,执行并返回结果moduleId
对应的模块被引用过,则直接返回存储在__webpack_module_cache__
缓存对象中的导出内容,避免重复执行其中,业务模块代码被存储在 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 _aWEBPACK_IMPORTED_MODULE_0 =
webpack_require(/! ./a _/ "./src/a.js");
console.log(_aWEBPACK_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_require__.e
:逻辑上包裹了一层中间件模式与promise.all
,用于异步加载多个模块__webpack_require__.f
:供__webpack_require__.e
使用的中间件对象,例如使用 Module Federation 特性时就需要在这里注册中间件以修改 e 函数的执行逻辑__webpack_require__.u
:用于拼接异步模块名称的函数__webpack_require__.l
:基于 JSONP 实现的异步模块加载函数__webpack_require__.p
:当前文件的完整 URL,可用于计算异步模块的实际 URL建议读者运行示例对比实际生成代码,感受它们的具体功能。这几个运行时模块构建起 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_require__.f
的中间件模式,以及用Promise.all
实现并行处理,实际加载工作由__webpack_require__.f.j
与__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__.f.j
实现了异步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
中通过 script 实现异步 chunk 内容的加载与执行。e + l + f.j
三个运行时函数支撑起 Webpack 异步模块运行的能力,落到实际用法上只需要调用 e 函数即可完成异步模块加载、运行,例如上例对应生成的entry
内容:/*!**********************!* !*** ./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, },
按照上述配置,使用命令
webpack serve --hot-only
启动 Webpack,就可以在 dist 文件夹找到产物:相比于前面两个示例,HMR 所产生运行时代码达到 1.5w+ 行,简直可以用炸裂来形容。主要的运行时内容有:
webpack-dev-server
、webpack/hot/xxx
、querystring
等框架,这一部分占了大部分代码__webpack_require__.l
:与异步模块加载一样,基于 JSONP 实现的异步模块加载函数__webpack_require__.e
:与异步模块加载一样__webpack_require__.f
:与异步模块加载一样__webpack_require__.hmrF
:用于拼接热更新模块 url 的函数webpack/runtime/hot
:这不是单个对象或函数,而是包含了一堆实现模块替换的方法可以看到, HMR 运行时是上面异步模块加载运行时的超集,而异步模块加载的运行时又是第一个基本示例运行时的超集,层层叠加。在 HMR 中包含了:
内容过多,我们放到下次专门开一篇文章聊聊 HMR。
仔细阅读上述三个示例,相信读者应该已经模模糊糊捕捉到一些重要规则:
__webpack_require__.e
函数,那么这里面必然有一个运行时依赖收集的过程落到 Webpack 源码实现上,运行时的生成逻辑可以划分为两个步骤:
两个步骤都发生在打包阶段,即 Webpack(v5) 源码的
compilation.seal
函数中:注意上图,进入 runtime 处理环节时 Webpack 已经解析得出
ModuleDependencyGraph
及ChunkGraph
关系,也就意味着此时已经可以计算出:chunk
chunk
包含那些module
,以及每个module
的内容chunk
与chunk
之间的父子依赖关系基于这些信息,接下来首先需要收集运行时依赖。
依赖收集
Webpack runtime 的依赖概念上很像 Vue 的依赖,都是用来表达模块对其它模块存在依附关系,只是实现方法上 Vue 基于动态、在运行过程中收集,而 Webpack 则基于静态代码分析的方式收集依赖。实现逻辑大致为:
运行时依赖的计算逻辑集中在
compilation.processRuntimeRequirements
函数,代码上包含三次循环:module
,收集所有module
的 runtime 依赖chunk
,将chunk
下所有module
的 runtime 统一收录到chunk
中chunk
下所有 runtime 依赖,之后遍历所有依赖并发布runtimeRequirementInTree
钩子,(主要是)RuntimePlugin
插件订阅该钩子并根据依赖类型创建对应的RuntimeModule
子类实例下面我们展开聊聊细节。
第一次循环:收集模块依赖
在打包 (seal) 阶段,完成
ChunkGraph
的构建之后,Webpack 会紧接着调用codeGeneration
函数遍历module
数组,调用它们的module.codeGeneration
函数执行模块转译,模块转译结果如:其中,sources 属性为模块经过转译后的结果;而
runtimeRequirements
则是基于 AST 计算出来的,为运行该模块时所需要用到的运行时,计算过程与本文主题无关,挖个坑下一回我们再继续讲。所有模块转译完毕后,开始调用
compilation.processRuntimeRequirements
进入第一重循环,将上述转译结果的runtimeRequirements
记录到ChunkGraph
对象中。第二次循环:整合 chunk 依赖
第一次循环针对
module
收集依赖,第二次循环则遍历chunk
数组,收集将其对应所有module
的 runtime 依赖,例如:示例图中,
module a
包含两个运行时依赖;module b
包含一个运行时依赖,则经过第二次循环整合后,对应的chunk
会包含两个模块对应的三个运行时依赖。第三次循环:依赖标识转 RuntimeModule 对象
源码中,第三次循环的代码最少但逻辑最复杂,大致上执行三个操作:
chunk
的 runtime 依赖runtimeRequirementInTree
钩子RuntimePlugin
监听钩子,并根据 runtime 依赖的标识信息创建对应的RuntimeModule
子类对象,并将对象加入到ModuleDepedencyGraph
和ChunkGraph
体系中管理至此,runtime 依赖完成了从
module
内容解析,到收集,到创建依赖对应的Module
子类,再将Module
加入到ModuleDepedencyGraph
/ChunkGraph
体系的全流程,业务代码及运行时代码对应的模块依赖关系图完全 ready,可以准备进入下一阶段 —— 生成最终产物。但在继续讲解产物逻辑之前,我们有必要先解决两个问题:
chunk
是什么关系RuntimeModule
?与普通Module
有什么区别总结:Chunk 与 Runtime Chunk
在上一篇文章 有点难的 webpack 知识点:Chunk 分包规则详解 我尝试完整地讲解 Webpack 默认分包规则,回顾一下在三种特定的情况下,Webpack 会创建新的
chunk
:chunk
对象,称之为initial chunk
chunk
对象,称之为async chunk
默认情况下
initial chunk
通常包含运行该 entry 所需要的所有 runtime 代码,但 webpack 5 之后出现的第三条规则打破了这一限制,允许开发者将 runtime 从initial chunk
中剥离出来独立为一个多 entry 间可共享的runtime chunk
。类似的,异步模块对应 runtime 代码大部分都被包含在对应的引用者身上,比如说:
`// a.js
export default 'a-module'
// index.js
// 异步引入 a 模块
import('./a').then(console.log)
`
在这个示例中,index 异步引入 a 模块,那么按默认分配规则会产生两个
chunk
:入口文件 index 对应的initial chunk
、异步模块 a 对应的async chunk
。此时从ChunkGraph
的角度看chunk[index]
为chunk[a]
的父级,运行时代码会被打入chunk[index]
,站在浏览器的角度,运行chunk[a]
之前必须先运行chunk[index]
,两者形成明显的父子关系。总结:RuntimeModule 体系
在最开始阅读 Webpack 源码的时候,我就觉得很奇怪,
Module
是 Webpack 资源管理的基本单位,但Module
底下总共衍生出了 54 个子类,且大部分为Module => RuntimeModule => xxxRuntimeModule
的继承关系:在 有点难的 webpack 知识点:Dependency Graph 深度解析 一文中我们聊到模块依赖关系图的生成过程及作用,但文章的内容主要围绕业务代码展开,用到的大多是
NormalModule
。到seal
函数收集运行时的过程中,RuntimePlugin
还会为运行时依赖一一创建对应的RuntimeModule
子类,例如:__webpack_require__.r
,则对应创建MakeNamespaceObjectRuntimeModule
对象__webpack_require__.o
,则对应创建HasOwnPropertyRuntimeModule
对象__webpack_require__.e
,则对应创建EnsureChunkRuntimeModule
对象所以可以推导出所有
RuntimeModule
结尾的类型与特定的运行时功能一一对应,收集依赖的结果就是在业务代码之外创建出一堆支撑性质的RuntimeModule
子类,这些子类对象随后被加入ModuleDependencyGraph
,并入整个模块依赖体系中。资源合并生成
经过上面的运行时依赖收集过程后,bundle 所需要的所有内容都就绪了,接着就可以准备写出到文件中,即下图核心流程中的生成 (emit) 阶段:
我的另一篇 [万字总结] 一文吃透 Webpack 核心原理 对这一块有比较细致的讲解,这里从运行时的视角再简单聊一下代码流程:
compilation.createChunkAssets
,遍历chunks
将 chunk 对应的所有module
,包括业务模块、运行时模块全部合并成一个资源 (Source
子类) 对象compilation.emitAsset
将资源对象挂载到compilation.assets
属性中compiler.emitAssets
将 assets 全部写到 FileSystemcompiler.hooks.done
钩子Webpack 真的很复杂,每次信心满满写出一个主题的内容之后都会发现更多新的坑点,比如本文可以衍生出来的关注点:
慢慢挖坑,慢慢填坑吧。如果觉得文章有用,请务必点赞关注转发来一波。
https://www.teqng.com/2021/08/06/webpack-%E5%8E%9F%E7%90%86%E7%B3%BB%E5%88%97%E5%85%AD%EF%BC%9A-%E5%BD%BB%E5%BA%95%E7%90%86%E8%A7%A3-webpack-%E8%BF%90%E8%A1%8C%E6%97%B6/
The text was updated successfully, but these errors were encountered: