外贸网站建设信息,网络推广方法怎么做,网站是哪家公司做的,小程序开店流程概述
回顾最近几节内容#xff0c;Webpack 运行过程中首先会根据 Module 之间的引用关系构建 ModuleGraph 对象#xff1b;接下来按照若干内置规则将 Module 组织进不同 Chunk 对象中#xff0c;形成 ChunkGraph 关系图。
接着#xff0c;构建流程将来到最后一个重要步骤…概述
回顾最近几节内容Webpack 运行过程中首先会根据 Module 之间的引用关系构建 ModuleGraph 对象接下来按照若干内置规则将 Module 组织进不同 Chunk 对象中形成 ChunkGraph 关系图。
接着构建流程将来到最后一个重要步骤生成产物代码这个过程会将所有 Module 内容一一转换为适当的产物代码形态并以 Chunk 为单位合并 Module 产物代码之后根据 Module 中出现的特性依赖补充相应运行时代码最终构建出我们日常所见的 Webpack Bundle 代码文件。
本文将深入分析这个过程的源码详细剖析模块转译、运行时依赖分析、产物合并的具体实现逻辑。
什么是模块转译
众所周知Webpack 的打包功能并不是将原始文件代码“复制-粘贴”到产物文件那么简单为了确保代码能在不同环境 —— 多种版本的浏览器、Node、Electron 等正常运行构建时需要对模块源码适当做一些转换操作这一点在大多数构建产物的内容中都有所体现例如
示例包含 index.js、name.js 两个 JS 代码模块经过 Webpack 构建后生成如图右侧所示的产物文件文件自上而下包含三块内容
name.js 模块对应的、函数形态的转译代码Webpack 按需注入的运行时代码index.js 模块对应的 IIFE立即执行函数 转译代码。
其中name.js、index.js 对应的产物代码与源码相比虽然语义与功能都基本相同但表现形式发生了较大变化例如 index.js 编译前后的内容
整个模块被包裹进 IIFE立即执行函数中添加 __webpack_require__.r(__webpack_exports__); 语句用于适配 ESM 规范源码中的 import 语句被转译为 __webpack_require__ 函数调用源码 console 语句所使用的 name 变量被转译为 _name__WEBPACK_IMPORTED_MODULE_0__.default 添加若干注释。
编译前后代码功能逻辑相同但替换掉这些 ES 高级特性之后却能让应用平稳运行在低版本浏览器中那么这种代码转换功能具体是怎么实现的呢
模块转译主流程
在前文《Webpack: 三种Chunk产物的打包逻辑》中我们已经介绍了 compilation.seal 函数内会调用 buildChunkGraph 生成 Chunk 依赖关系图之后 Webpack 就可以分析出
需要输出那些 Chunk每个 Chunk 包含那些 Module以及每个 Module 经过 Loader 翻译后的代码内容Chunk 与 Chunk 之间的父子依赖关系。
在此之后 seal 函数会开始触发一堆优化钩子借助插件对 ChunkGraph 做诸如合并、拆分、删除无效 Chunk 等优化操作并在最后调用 compilation.codeGeneration 方法
class Compilation {seal(callback) {// 初始化 ChunkGraph、ChunkGroup 对象for (const [name, { dependencies, includeDependencies, options }] of this.entries) {// ...}for (const [name,{options: { dependOn, runtime },},] of this.entries) {// ...}// 构建 ChunkGroupbuildChunkGraph(this, chunkGraphInit);// 执行诸多优化钩子this.hooks.optimize.call();// ...this.hooks.optimizeTree.callAsync(this.chunks, this.modules, (err) {// ...this.hooks.optimizeChunkModules.callAsync(this.chunks, this.modules, (err) {// ...this.hooks.beforeCodeGeneration.call();// 开始生成最终产物代码this.codeGeneration(/* ... */);});});}
}codeGeneration 方法负责生成最终的资产代码主要流程 有三个关键步骤。 单模块转译这一步主要用于计算模块实际输出代码遍历 compilation.modules 数组调用 module 对象的 codeGeneration 方法执行模块转译计算 调用 JavascriptGenerator 的 generate 方法 遍历 module 对象的 dependencies 与 presentationalDependencies 数组 执行 每个数组项 dependeny 对象对应的 template.apply 方法方法中视情况可能产生三种副作用 直接修改模块 source 数据如 ConstDependency.Template将结果记录到 initFragments 数组如 HarmonyExportSpecifierDependency将运行时依赖记录到 runtimeRequirements 数组如 HarmonyImportDependency。 收集运行时依赖计算模块运行时首先调用 compilation.processRuntimeRequirements 方法将上一步生成的 runtimeRequirements 数组一一转换为 RuntimeModule 对象并挂载到 ChunkGroup 中。 模块合并调用 compilation.createChunkAssets 方法以 Chunk 为单位将相应的所有 module 及 runtimeModule 按规则塞进「产物框架」 中最终合并输出成完整的 Bundle 文件。
这些就是 Webpack 最终消费 ModuleGraph 与 ChunkGraph生成最终产物代码的关键过程总结而言就是先遍历所有模块依赖对象收集模块编译结果与运行时依赖之后将这些内容合并在一起输出为 Bundle 文件。
下面我们逐一展开了解每个步骤的细节。
单模块转译
「模块转译」 操作从 module.codeGeneration 调用开始对应到上述流程图的 这个过程首先调用 JavascriptGenerator.generate 函数遍历模块的 dependencies 数组依次调用依赖对象对应的 Template 子类 apply 方法更新模块内容说起来有点绕我将重要步骤抽取为如下伪代码
class JavascriptGenerator {generate(module, generateContext) {// 先取出 module 的原始代码内容const source new ReplaceSource(module.originalSource());const { dependencies, presentationalDependencies } module;const initFragments [];for (const dependency of [...dependencies, ...presentationalDependencies]) {// 找到 dependency 对应的 templateconst template generateContext.dependencyTemplates.get(dependency.constructor);// 调用 template.apply传入 source、initFragments// 在 apply 函数可以直接修改 source 内容或者更改 initFragments 数组影响后续转译逻辑template.apply(dependency, source, {initFragments})}// 遍历完毕后调用 InitFragment.addToSource 合并 source 与 initFragmentsreturn InitFragment.addToSource(source, initFragments, generateContext);}
}// Dependency 子类
class xxxDependency extends Dependency {}// Dependency 子类对应的 Template 定义
const xxxDependency.Template class xxxDependencyTemplate extends Template {apply(dep, source, {initFragments}) {// 1. 直接操作 source更改模块代码source.replace(dep.range[0], dep.range[1] - 1, some thing)// 2. 通过添加 InitFragment 实例补充代码initFragments.push(new xxxInitFragment())}
}从上述伪代码可以看出JavascriptGenerator.generate 函数的逻辑相对比较固化
初始化 source、initFragments 等变量遍历 module 对象的依赖数组找到每个 dependency 对应的 template 对象调用 template.apply 函数修改模块内容调用 InitFragment.addToSource 方法合并 source 与 initFragments 数组生成最终结果。
这里的重点是 JavascriptGenerator.generate 函数并不操作 module 源码它仅仅提供一个执行框架真正处理模块内容转译的逻辑都在 xxxDependencyTemplate 对象的 apply 函数实现如上例伪代码中 24-28 行。
每个 Dependency 子类都会挂载一个 Template 子类且通常这两个类都会写在同一个文件中例如 ConstDependency 与 ConstDependencyTemplateNullDependency 与 NullDependencyTemplate。
Webpack 从「构建」(make) 阶段开始就会通过 Dependency 子类记录不同情况下模块之间的依赖关系到「封装」(seal) 阶段再通过 Template 子类修改 module 代码最终 Module、Template、 JavascriptGenerator、Dependency 四个关键类形成如下交互关系 Template 对象会通过三种方法影响产物代码
直接操作 source 对象修改模块代码该对象最初的内容等于模块的源码经过多个 Template.apply 函数流转后逐渐被替换成新的代码形式操作 initFragments 数组在模块源码之外插入补充代码片段将运行时依赖记录到 runtimeRequirements 数组。
其中第 1、2 种操作所产生的副作用最终都会被传入 InitFragment.addToSource 函数合并成最终结果。
通过 source 修改模块代码
先来看看 source 操作webpack-sources 是 Webpack 中用于编辑字符串的一套工具类库它提供了一系列代码编辑方法包括
字符串合并、替换、插入等模块代码缓存、sourcemap 映射、hash 计算等。
Webpack 内部以及社区的很多插件、loader 都会使用 webpack-sources 库编辑代码内容包括上文介绍的 Template.apply 体系。逻辑上在启动模块代码生成流程时Webpack 会先用模块原始内容初始化 Source 对象即
const source new ReplaceSource(module.originalSource());之后不同 Dependency 子类按序、按需更改 source 内容例如 HarmonyImportSpecifierDependency 中
HarmonyImportSpecifierDependency.Template class HarmonyImportSpecifierDependencyTemplate extends (HarmonyImportDependency.Template
) {apply(dependency, source, templateContext) {const dep /** type {HarmonyImportSpecifierDependency} */ (dependency);// ...const ids dep.getIds(moduleGraph);const exportExpr this._getCodeForIds(dep, source, templateContext, ids);const range dep.range;if (dep.shorthand) {source.insert(range[1], : ${exportExpr});} else {source.replace(range[0], range[1] - 1, exportExpr);}}
};举个例子对于下面这段简单代码
import bar from ./bar;
console.log(bar);会产生 HarmonyImportSpecifierDependency 与 ConstDependency 两个依赖对象之后
import bar from ./bar;
console.log(bar);// 首先HarmonyImportSpecifierDependency 替换导入变量名
import bar from ./bar;
console.log(_bar__WEBPACK_IMPORTED_MODULE_1__[default]);// 之后ConstDependency 删除模块导入语句
console.log(_bar__WEBPACK_IMPORTED_MODULE_1__[default]);可以看出这部分逻辑的效果与 Babel 类似会直接修改模块源码实现语言层面的向下兼容。但这还不够还需要将这段代码包裹进 Webpack 的模块框架中这部分工作将由 initFragments 数组完成。
initFragments 数组的作用
上面我们聊到除直接操作 source 外Template.apply 中还可能通过 initFragments 数组达成修改模块产物的效果。initFragments 数组项为 InitFragment 子类实例它们带有两个关键函数getContent、getEndContent分别用于获取代码片段的头尾部分。
例如 HarmonyImportDependencyTemplate 的 apply 函数中
HarmonyImportDependency.Template class HarmonyImportDependencyTemplate extends (ModuleDependency.Template
) {apply(dependency, source, templateContext) {// ...templateContext.initFragments.push(new ConditionalInitFragment(importStatement[0] importStatement[1],InitFragment.STAGE_HARMONY_IMPORTS,dep.sourceOrder,key,runtimeCondition));//...}}也就是根据模块需求不断增加新的代码片段 initFragments所有 Dependency 执行完毕后接着就需要调用 InitFragment.addToSource 函数将两者合并为模块产物。addToSource 的核心代码如下
class InitFragment {static addToSource(source, initFragments, generateContext) {// 先排好顺序const sortedFragments initFragments.map(extractFragmentIndex).sort(sortFragmentWithIndex);// ...const concatSource new ConcatSource();const endContents [];for (const fragment of sortedFragments) {// 合并 fragment.getContent 取出的片段内容concatSource.add(fragment.getContent(generateContext));const endContent fragment.getEndContent(generateContext);if (endContent) {endContents.push(endContent);}}// 合并 sourceconcatSource.add(source);// 合并 fragment.getEndContent 取出的片段内容for (const content of endContents.reverse()) {concatSource.add(content);}return concatSource;}
}可以看到addToSource 函数的逻辑
遍历 initFragments 数组按顺序合并 fragment.getContent() 的产物合并 source 对象遍历 initFragments 数组按顺序合并 fragment.getEndContent() 的产物。
所以模块代码合并操作主要就是用 initFragments 数组一层一层包裹住模块代码 source而两者都在 Template.apply 层面维护。还是上面那个简单例子经过这段 Template 处理后最终转化为
import bar from ./bar;
console.log(bar);// 首先HarmonyImportSpecifierDependency 替换导入变量名
import bar from ./bar;
console.log(_bar__WEBPACK_IMPORTED_MODULE_1__[default]);// 之后ConstDependency 删除模块导入语句
console.log(_bar__WEBPACK_IMPORTED_MODULE_1__[default]);// 经过 ConditionalInitFragment 处理
/* harmony import */ var _bar__WEBPACK_IMPORTED_MODULE_1__ __webpack_require__(/*! ./bar */ ./src/bar.js);
console.log(_bar__WEBPACK_IMPORTED_MODULE_1__[default]);简单总结一下Webpack 生成 ModuleGraph 与 ChunkGraph 后会立即开始遍历所有 Dependency 对象依次调用对象的静态方法 template.apply 修改 module 代码最后再将所有变更后的 source 与模块脚手架 initFragments 合并为最终产物完成从单个模块的源码形态到产物形态的转变。
自定义 Template.apply 示例
「模块转译」 步骤流程比较长整体逻辑很复杂为了加深理解接下来我们尝试开发一个简单的 Banner 插件实现在每个模块前自动插入一段字符串。实现上插件主要涉及 Dependency、Template、hooks 对象代码
const { Dependency, Template } require(webpack);class DemoDependency extends Dependency {constructor() {super();}
}DemoDependency.Template class DemoDependencyTemplate extends Template {apply(dependency, source) {const today new Date().toLocaleDateString();source.insert(0, /* Author: Tecvan */
/* Date: ${today} */
);}
};module.exports class DemoPlugin {apply(compiler) {compiler.hooks.thisCompilation.tap(DemoPlugin, (compilation) {// 调用 dependencyTemplates 注册 Dependency 到 Template 的映射compilation.dependencyTemplates.set(DemoDependency,new DemoDependency.Template());compilation.hooks.succeedModule.tap(DemoPlugin, (module) {// 模块构建完毕后插入 DemoDependency 对象module.addDependency(new DemoDependency());});});}
};示例插件的关键步骤
编写 DemoDependency 与 DemoDependencyTemplate 类其中 DemoDependency 仅做示例用没有实际功能DemoDependencyTemplate 则在其 apply 中调用 source.insert 插入字符串如示例代码第 10-14 行使用 compilation.dependencyTemplates 注册 DemoDependency 与 DemoDependencyTemplate 的映射关系使用 thisCompilation 钩子取得 compilation 对象使用 succeedModule 钩子订阅 module 构建完毕事件并调用 module.addDependency 方法添加 DemoDependency 依赖。
完成上述操作后module 对象的产物在生成过程就会调用到 DemoDependencyTemplate.apply 函数插入我们定义好的字符串效果如
感兴趣的同学也可以直接阅读 Webpack 仓库的如下文件学习更多用例
ConstDependency一个简单示例可学习 source 的更多操作方法HarmonyExportSpecifierDependency一个较简单的示例可学习 initFragments 数组的更多用法HarmonyImportDependency一个较复杂但使用率极高的示例可综合学习 source、initFragments 数组的用法。
收集运行时模块
为了正常、正确运行业务项目Webpack 需要将开发者编写的业务代码以及支撑、调配这些业务代码的 运行时 一并打包到产物bundle中以建筑作类比的话业务代码相当于砖瓦水泥是看得见摸得着能直接感知的逻辑运行时相当于掩埋在砖瓦之下的钢筋地基通常不需要关注但决定了整座建筑的功能、质量。
大多数 Webpack 特性都需要特定钢筋地基才能跑起来包括异步加载、HMR、WASM、Module Federation 等。即使没有用到这些特性仅仅是最简单的模块导入导出也都需要生成若干模拟 CMD 模块化方案运行时代码例如
// a.js
export default a module;// index.js
import name from ./a
console.log(name)打包结果
可以看出整个 Bundle 被包裹在一个立即执行函数中函数内部从上到下依次定义
__webpack_modules__ 对象包含了除入口外的所有模块如示例中的 a.js 模块__webpack_module_cache__ 对象用于存储被引用过的模块__webpack_require__ 函数实现模块引用(require) 逻辑__webpack_require__.d 工具函数实现将模块导出的内容附加的模块对象上__webpack_require__.o 工具函数判断对象属性用__webpack_require__.r 工具函数在 ESM 模式下声明 ESM 模块标识最后的 IIFE对应 entry 模块即上述示例的 index.js 用于启动整个应用。
这几个 __webpack_ 开头奇奇怪怪的函数可以统称为 Webpack 运行时代码作用如前面所说的是搭起整个业务项目的骨架就上述简单示例所罗列出来的几个函数、对象而言它们协作构建起一个简单的模块化体系从而实现 ES Module 规范所声明的模块化特性。
上述函数、对象构成了 Webpack 运行时最基本的能力 —— 模块化假如代码中用到更多 Webpack 特性则会相应地注入更多运行时模块代码例如
使用异步加载时注入 __webpack_require__.e、__webpack_require__.f 等模块使用 HMR 时注入 __webpack_require__.hmrF、webpack/runtime/hot 等模块。
那么Webpack 是如何收集运行时依赖并将之合并到最终产物中的呢
收集运行时依赖
早在「构建」阶段Webpack 就已经开始在持续收集运行时依赖例如在一个非常简单的模块导入语句中
import bar from ./bar;Webpack 在处理上述代码 AST 时会相应生成多个依赖对象比较重要的有
HarmonyImportSideEffectDependency主要的 Dependency 对象Webpack 会为该对象创建相应的 NormalModule 实例从而递归处理新模块代码HarmonyCompatibilityDependency运行时模块依赖对应的 Template.apply 函数会在生成代码时记录相应运行时需求。
本质上这是一个基于静态代码分析的方式收集依赖的过程。当所有模块处理完毕收集到所有运行时依赖进入 codeGeneration 函数后Webpack 会进一步将这些依赖对象挂载到 Chunk 中
这个过程集中 compilation.processRuntimeRequirements 函数函数中包含三次循环
第一次循环遍历所有 module收集所有 module 的 runtime 依赖第二次循环遍历所有 chunk将 chunk 下所有 module 的 runtime 统一收录到 chunk 中第三次循环遍历所有 runtime chunk收集其对应的子 chunk 下所有 runtime 依赖之后遍历所有依赖并发布 runtimeRequirementInTree 钩子主要是 RuntimePlugin 插件订阅该钩子并根据依赖类型创建对应的 RuntimeModule 子类实例。
第一次循环收集模块依赖
在上述「模块转译主流程」中我们聊到 Template.apply 函数可能修改模块的 runtimeRequirements 数组最终形成如下结构
这个过程相当于将模块的 Runtime Dependency 都转化为 __webpack_require__ 等枚举值并调用 compilation.processRuntimeRequirements 进入第一重循环将上述 runtimeRequirements 数组 挂载 到 ChunkGraph 对象中。
第二次循环整合 chunk 依赖
第一次循环针对 module 收集依赖第二次循环则遍历 chunk 数组收集将其对应所有 module 的 runtime 依赖例如
示例图中module a 包含两个运行时依赖module b 包含一个运行时依赖则经过第二次循环整合后对应的 chunk 会包含两个模块所包含的三个运行时依赖。
第三次循环依赖标识转 RuntimeModule 对象
源码中第三次循环的代码最少但逻辑最复杂大致上执行三个操作
遍历所有 runtime chunk收集其所有子 chunk 的 runtime 依赖为该 runtime chunk 下的所有依赖发布 runtimeRequirementInTree 钩子RuntimePlugin 监听钩子并根据 runtime 依赖的标识信息创建对应的 RuntimeModule 子类对象并将对象加入到 ModuleDepedencyGraph /ChunkGraph 体系中管理。
至此runtime 依赖完成了从 module 内容解析到收集到创建依赖对应的 Module 子类再将 Module 加入到 ModuleDepedencyGraph /ChunkGraph 体系的全流程业务代码及运行时代码对应的模块依赖关系图完全 ready可以准备进入下一阶段 —— 合并最终产物。
合并最终产物
讲完单个模块转译以及运行时模块收集过程后我们终于来到最后一步 流程图中compilation.codeGeneration 函数执行完毕 —— 也就是模块转译阶段完成后模块的转译结果会一一保存到 compilation.codeGenerationResults 对象中之后会启动一个新的执行流程 —— 模块合并打包。
模块合并打包过程会将 chunk 对应的 module 及 runtimeModule 按规则塞进模板框架中最终合并输出成完整的 bundle 文件例如上例中
示例右边 bundle 文件中红框框出来的部分为用户代码文件及运行时模块生成的产物其余部分撑起了一个 IIFE 形式的运行框架即为模板框架也就是
(() { // webpackBootstrapuse strict;var __webpack_modules__ ({module-a: ((__unused_webpack_module, __webpack_exports__, __webpack_require__) {// ! module 代码}),module-b: ((__unused_webpack_module, __webpack_exports__, __webpack_require__) {// ! module 代码})});// The module cachevar __webpack_module_cache__ {};// The require functionfunction __webpack_require__(moduleId) {// ! webpack CMD 实现}/************************************************************************/// ! 各种 runtime/************************************************************************/var __webpack_exports__ {};// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.(() {// ! entry 模块})();
})();捋一下这里的逻辑运行框架包含如下关键部分
最外层是一个 IIFE 包裹一个记录了除 entry 外的其它模块代码的 __webpack_modules__ 对象对象的 key 为模块标志符值为模块转译后的代码一个极度简化的 CMD 实现 __webpack_require__ 函数最后一个包裹了 entry 代码的 IIFE 函数。
模块转译 是将 module 转译为可以在宿主环境如浏览器上运行的代码形式收集运行时模块 负责决定整个 Bundle 需要的骨架代码而 模块合并 操作则串联这些 modules 使之整体符合开发预期能够正常运行整个应用逻辑。接下来我们揭晓这部分代码的生成原理。
模块合并主流程
在 compilation.codeGeneration 执行完毕即所有用户代码模块做完转译运行时模块都收集完毕作后seal 函数调用 compilation.createChunkAssets 函数触发 renderManifest 钩子JavascriptModulesPlugin 插件监听到这个钩子消息后开始组装 bundle伪代码
// Webpack 5
// lib/Compilation.js
class Compilation {seal() {// 先把所有模块的代码都转译准备好this.codeGenerationResults this.codeGeneration(this.modules);// 1. 调用 createChunkAssetsthis.createChunkAssets();}createChunkAssets() {// 遍历 chunks 为每个 chunk 执行 render 操作for (const chunk of this.chunks) {// 2. 触发 renderManifest 钩子const res this.hooks.renderManifest.call([], {chunk,codeGenerationResults: this.codeGenerationResults,...others,});// 提交组装结果this.emitAsset(res.render(), ...others);}}
}// lib/javascript/JavascriptModulesPlugin.js
class JavascriptModulesPlugin {apply() {compiler.hooks.compilation.tap(JavascriptModulesPlugin, (compilation) {compilation.hooks.renderManifest.tap(JavascriptModulesPlugin, (result, options) {// JavascriptModulesPlugin 插件中通过 renderManifest 钩子返回组装函数 renderconst render () // render 内部根据 chunk 内容选择使用模板 renderMain 或 renderChunk// 3. 监听钩子返回打包函数this.renderMain(options);result.push({ render /* arguments */ });return result;});});}renderMain() {/* */}renderChunk() {/* */}
}这里的核心逻辑是compilation 以 renderManifest 钩子方式对外发布 bundle 打包需求 JavascriptModulesPlugin 监听这个钩子按照 chunk 的内容特性调用不同的打包函数。
提示上述仅针对 Webpack5 有效在 Webpack4 中打包逻辑集中在 MainTemplate 完成。
JavascriptModulesPlugin 内置的打包函数有
renderMain打包主 chunk 时使用renderChunk打包子 chunk 如异步模块 chunk 时使用。
两个打包函数实现的逻辑接近都是按顺序拼接各个模块下面简单介绍下 renderMain 的实现。
JavascriptModulesPlugin.renderMain 函数
renderMain 函数涉及比较多场景判断原始代码很长很绕我摘了几个重点步骤
class JavascriptModulesPlugin {renderMain(renderContext, hooks, compilation) {const { chunk, chunkGraph, runtimeTemplate } renderContext;const source new ConcatSource();// ...// 1. 先计算出 bundle CMD 核心代码包含// - var __webpack_module_cache__ {}; 语句// - __webpack_require__ 函数const bootstrap this.renderBootstrap(renderContext, hooks);// 2. 计算出当前 chunk 下除 entry 外其它模块的代码const chunkModules Template.renderChunkModules(renderContext,inlinedModules? allModules.filter((m) !inlinedModules.has(m)): allModules,(module) this.renderModule(module,renderContext,hooks,allStrict ? strict : true),prefix);// 3. 计算出运行时模块代码const runtimeModules renderContext.chunkGraph.getChunkRuntimeModulesInOrder(chunk);// 4. 重点来了开始拼接 bundle// 4.1 首先合并核心 CMD 实现即上述 bootstrap 代码const beforeStartup Template.asString(bootstrap.beforeStartup) \n;source.add(new PrefixSource(prefix,useSourceMap? new OriginalSource(beforeStartup, webpack/before-startup): new RawSource(beforeStartup)));// 4.2 合并 runtime 模块代码if (runtimeModules.length 0) {for (const module of runtimeModules) {compilation.codeGeneratedModules.add(module);}}// 4.3 合并除 entry 外其它模块代码for (const m of chunkModules) {const renderedModule this.renderModule(m, renderContext, hooks, false);source.add(renderedModule)}// 4.4 合并 entry 模块代码if (hasEntryModules runtimeRequirements.has(RuntimeGlobals.returnExportsFromRuntime)) {source.add(${prefix}return __webpack_exports__;\n);}return source;}
}核心逻辑为
先计算出 bundle CMD 代码即 __webpack_require__ 函数计算出当前 chunk 下除 entry 外其它模块代码 chunkModules计算出运行时模块代码开始执行合并操作子步骤有 合并 CMD 代码合并 runtime 模块代码遍历 chunkModules 变量合并除 entry 外其它模块代码合并 entry 模块代码。 返回结果。
总结先计算出不同组成部分的产物形态之后按顺序拼接打包输出合并后的版本。
至此Webpack 完成 bundle 的转译、打包流程后续调用 compilation.emitAsset将产物内容输出到 output 指定的路径即可Webpack 单次编译打包过程就结束了。
总结
从《Webpack: 核心流程之Init、Make、Seal》开始我们花了四节篇幅终于讲完了 Webpack 构建主流程中方方面面的原理划重点
Webpack 构建过程可以简单划分为 Init、Make、Seal 三个阶段Init 阶段负责初始化 Webpack 内部若干插件与状态逻辑比较简单Make 阶段解决资源读入问题这个阶段会从 Entry —— 入口模块开始递归读入、解析所有模块内容并根据模块之间的依赖关系构建 ModuleGraph —— 模块关系图对象Seal 阶段更复杂 一方面根据 ModuleGraph 构建 ChunkGraph另一方面开始遍历 ChunkGraph转译每一个模块代码最后将所有模块与模块运行时依赖合并为最终输出的 Bundle —— 资产文件。
这些内容都是介绍 Webpack 实现原理的文章可能并不能马上解决你在业务中正在面临的现实问题但放到更长的时间维度这些内容所呈现的知识、思维、思辨过程可能能够长远地给到你
分析、理解复杂开源代码的能力理解 Webpack 架构及实现细节下次遇到问题的时候能根据表象迅速定位到根源理解 Webpack 为 hooks、loader 提供的上下文能够更通畅地理解其它开源组件甚至能够自如地实现自己的组件。
所以希望你能沿着这个思路反复、仔细阅读这些章节深入理解底层实现原理成为真正意义上的 Webpack 专家。
思考 Dependency、Module 之间是什么关系为什么需要设计 Dependency 这个看似可有可无的结构