Webpack模块编译打包及运行时(Runtime)逻辑深度解析
Webpack模块编译打包及运行时(Runtime)逻辑深度解析
Webpack在构建过程中,会将所有Module内容转换为适当的产物代码形态,并以Chunk为单位合并Module产物代码,最终构建出Webpack Bundle代码文件。本文将深入分析这个过程的源码,详细剖析模块转译、运行时依赖分析、产物合并的具体实现逻辑。
什么是模块转译?
Webpack的打包功能并不是将原始文件代码“复制-粘贴”到产物文件那么简单,为了确保代码能在不同环境(多种版本的浏览器、Node、Electron等)正常运行,构建时需要对模块源码适当做一些转换操作。例如:
示例包含index.js、name.js两个JS代码模块,经过Webpack构建后生成如图右侧所示的产物文件,文件自上而下包含三块内容:
- name.js模块对应的、函数形态的转译代码;
- Webpack按需注入的运行时代码;
- index.js模块对应的IIFE(立即执行函数)转译代码。
其中,index.js编译前后的内容变化包括:
- 整个模块被包裹进IIFE(立即执行函数)中;
- 添加__webpack_require__.r(webpack_exports);语句,用于适配ESM规范;
- 源码中的import语句被转译为__webpack_require__函数调用;
- 源码console语句所使用的name变量被转译为_name__WEBPACK_IMPORTED_MODULE_0__.default;
- 添加若干注释。
模块转译主流程
在前文《Webpack: 三种Chunk产物的打包逻辑》中,我们已经介绍了compilation.seal函数内会调用buildChunkGraph生成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) {
// ...
}
// 构建 ChunkGroup
buildChunkGraph(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方法,执行模块转译计算;
- 收集运行时依赖:计算模块运行时,首先调用compilation.processRuntimeRequirements方法,将上一步生成的runtimeRequirements数组一一转换为RuntimeModule对象,并挂载到ChunkGroup中;
- 模块合并:调用compilation.createChunkAssets方法,以Chunk为单位,将相应的所有module及runtimeModule按规则塞进「产物框架」中,最终合并输出成完整的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 对应的 template
const template = generateContext.dependencyTemplates.get(dependency.constructor);
// 调用 template.apply,传入 source、initFragments
// 在 apply 函数可以直接修改 source 内容,或者更改 initFragments 数组,影响后续转译逻辑
template.apply(dependency, source, {initFragments})
}
// 遍历完毕后,调用 InitFragment.addToSource 合并 source 与 initFragments
return 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函数并不操作module源码,它仅仅提供一个执行框架,真正处理模块内容转译的逻辑都在xxxDependencyTemplate对象的apply函数实现。
收集运行时模块
为了正常、正确运行业务项目,Webpack需要将开发者编写的业务代码以及支撑、调配这些业务代码的运行时一并打包到产物(bundle)中。大多数Webpack特性都需要特定运行时才能跑起来,包括异步加载、HMR、WASM、Module Federation等。
Webpack收集运行时依赖的过程主要集中在compilation.processRuntimeRequirements函数中,函数中包含三次循环:
- 第一次循环遍历所有module,收集所有module的runtime依赖;
- 第二次循环遍历所有chunk,将chunk下所有module的runtime统一收录到chunk中;
- 第三次循环遍历所有runtime chunk,收集其对应的子chunk下所有runtime依赖,之后遍历所有依赖并发布runtimeRequirementInTree钩子,主要是RuntimePlugin插件订阅该钩子并根据依赖类型创建对应的RuntimeModule子类实例。
合并最终产物
模块合并主流程由compilation.createChunkAssets函数触发,该函数通过renderManifest钩子对外发布bundle打包需求。JavascriptModulesPlugin监听这个钩子,按照chunk的内容特性,调用不同的打包函数。核心逻辑包括:
- 计算出bundle CMD核心代码,包含__webpack_module_cache__对象和__webpack_require__函数;
- 计算出当前chunk下,除entry外其他模块的代码;
- 计算出运行时模块代码;
- 按顺序拼接bundle,包括CMD实现、runtime模块代码、其他模块代码和entry模块代码。
总结
Webpack构建过程可以简单划分为Init、Make、Seal三个阶段:
- Init阶段负责初始化Webpack内部若干插件与状态;
- Make阶段解决资源读入问题,递归读入、解析所有模块内容,并构建ModuleGraph;
- Seal阶段更复杂:一方面构建ChunkGraph,另一方面转译每个模块代码,最后合并为Bundle文件。
通过深入理解Webpack的工作原理,可以帮助开发者更好地分析和解决实际开发中的问题,提升对Webpack架构和实现细节的理解。