Runtime:模块编译打包及运行时逻辑
📋 版本差异对照表
| 特性 | v1 (原始版) | v2 (当前版) | |------|------------|------------| | 基准版本 | Webpack 5.x (未明确) | Webpack 5.107.0 | | 运行时函数 | 基础介绍 | 完整 __webpack_require__ 函数族详解 + Mermaid 流程图 | | ESM Output | 未涉及 | 新增 output.module: true 时原生 ESM 运行时分析 | | RuntimeChunk | 简要提及 | 三种提取策略(single/multiple/false)深度对比 | | 异步加载 | JSONP 概述 | JSONP + Import Scripts 双模式 + ESM dynamic boundary | | Scope Hoisting | 未详细展开 | 对运行时的性能影响及变量提升机制 | | 可视化图表 | 截图为主 | 新增 2 个 Mermaid 流程图 | | 新特性覆盖 | - | v5.107 __webpack_require__.dn 辅助函数 |
核心概念回顾
在前面章节中,我们已经介绍了 Webpack 从模块解析到 Chunk 封装的完整流程。构建过程会根据 Module 之间的引用关系构建 ModuleGraph 对象;接下来按照内置规则将 Module 组织进不同 Chunk 对象中,形成 ChunkGraph 关系图。
接下来,构建流程将来到最后一个重要步骤:生成产物代码。这个过程会将所有 Module 内容转换为适当的产物代码形态,并以 Chunk 为单位合并 Module 产物代码,之后根据 Module 中出现的特性依赖,补充相应运行时代码,最终构建出我们日常所见的 Webpack Bundle 代码文件。
什么是模块转译?
众所周知,Webpack 的打包功能并不是将原始文件代码"复制-粘贴"到产物文件那么简单。为了确保代码能在不同环境 —— 多种版本的浏览器、Node.js、Electron 等正常运行,构建时需要对模块源码适当做一些转换操作。
转译示例
假设有以下两个 JS 代码模块:
// name.js
export default 'tecvan';
// index.js
import name from './name';
console.log(`hello ${name}`);经过 Webpack 构建后生成的产物文件包含三块内容:
name.js模块对应的转译代码:被包裹进 IIFE(立即执行函数)- Webpack 按需注入的运行时代码
index.js模块对应的 IIFE 转译代码
编译前后代码功能逻辑相同,但表现形式发生了较大变化:
- 整个模块被包裹进 IIFE 中
- 添加
__webpack_require__.r(__webpack_exports__)语句,用于适配 ESM 规范 - 源码中的
import语句被转译为__webpack_require__函数调用 - 源码
console语句所使用的name变量被转译为_name__WEBPACK_IMPORTED_MODULE_0__.default - 添加若干注释
这种代码转换功能具体是怎么实现的呢?让我们深入源码一探究竟。
模块转译主流程
在上一章《Chunk:三种产物的打包逻辑》中,我们已介绍 compilation.seal 函数内会调用 buildChunkGraph 生成 Chunk 依赖关系图。在此之后,seal 函数会触发一堆优化钩子,借助插件对 ChunkGraph 做优化操作,并在最后调用 compilation.codeGeneration 方法:
// Webpack 5.107
// lib/Compilation.js
class Compilation {
seal(callback) {
// 初始化 ChunkGraph、ChunkGroup 对象
for (const [name, { dependencies, includeDependencies, options }] 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 方法,执行模块转译计算:
- 调用
JavascriptGenerator的 generate 方法 - 遍历
module对象的dependencies与presentationalDependencies数组 - 执行每个数组项
dependency对象对应的template.apply方法,方法中视情况可能产生三种副作用:- 直接修改模块
source数据,如 ConstDependency.Template - 将结果记录到
initFragments数组如 HarmonyExportSpecifierDependency - 将运行时依赖记录到
runtimeRequirements数组如 HarmonyImportDependency
- 直接修改模块
步骤二:收集运行时依赖
计算模块运行时,首先调用 compilation.processRuntimeRequirements 方法,将上一步生成的 runtimeRequirements 数组一一转换为 RuntimeModule 对象,并挂载到 ChunkGroup 中。
步骤三:模块合并
调用 compilation.createChunkAssets 方法,以 Chunk 为单位,将相应的所有 module 及 runtimeModule 按规则塞进「产物框架」中,最终合并输出成完整的 Bundle 文件。
单模块转译深度解析
「模块转译」操作从 module.codeGeneration 调用开始。这个过程首先调用 JavascriptGenerator.generate 函数,遍历模块的 dependencies 数组,依次调用依赖对象对应的 Template 子类 apply 方法更新模块内容。
JavascriptGenerator.generate 核心伪代码
// Webpack 5.107
// lib/javascript/JavascriptGenerator.js
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
template.apply(dependency, source, {initFragments})
}
// 遍历完毕后,调用 InitFragment.addToSource 合并 source 与 initFragments
return InitFragment.addToSource(source, initFragments, generateContext);
}
}
// Dependency 子类示例
class HarmonyImportSpecifierDependency extends Dependency {}
// Dependency 子类对应的 Template 定义
HarmonyImportSpecifierDependency.Template = class extends Template {
apply(dep, 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);
}
}
};从上述伪代码可以看出,JavascriptGenerator.generate 函数的逻辑相对比较固化:
- 初始化
source、initFragments等变量 - 遍历
module对象的依赖数组,找到每个dependency对应的template对象,调用template.apply函数修改模块内容 - 调用
InitFragment.addToSource方法,合并source与initFragments数组,生成最终结果
关键点:JavascriptGenerator.generate 函数并不直接操作 module 源码,它仅仅提供一个执行框架,真正处理模块内容转译的逻辑都在各 xxxDependencyTemplate 对象的 apply 函数实现。
Template 对象的三种影响方式
Template 对象会通过三种方法影响产物代码:
- 直接操作
source对象:修改模块代码,该对象最初的内容等于模块的源码,经过多个Template.apply函数流转后逐渐被替换成新的代码形式 - 操作
initFragments数组:在模块源码之外插入补充代码片段 - 将运行时依赖记录到
runtimeRequirements数组
其中第 1、2 种操作所产生的副作用,最终都会被传入 InitFragment.addToSource 函数,合并成最终结果。
通过 source 修改模块代码
webpack-sources 是 Webpack 中用于编辑字符串的一套工具类库,它提供了一系列代码编辑方法:
- 字符串合并、替换、插入等
- 模块代码缓存、sourcemap 映射、hash 计算等
逻辑上,在启动模块代码生成流程时,Webpack 会先用模块原始内容初始化 Source 对象:
const source = new ReplaceSource(module.originalSource());之后,不同 Dependency 子类按序、按需更改 source 内容。例如对于下面的简单代码:
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 类似,会直接修改模块源码,实现语言层面的向下兼容。
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 函数将两者合并为模块产物:
// Webpack 5.107
// lib/InitFragment.js
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);
}
}
// 合并 source
concatSource.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 处理后,最终转化为:
// 最终产物:
/* harmony import */ var _bar__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./bar */ "./src/bar.js");
console.log(_bar__WEBPACK_IMPORTED_MODULE_1__["default"]);自定义 Template.apply 示例
为了加深理解,接下来我们尝试开发一个简单的 Banner 插件:实现在每个模块前自动插入一段字符串:
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) => {
compilation.dependencyTemplates.set(
DemoDependency,
new DemoDependency.Template()
);
compilation.hooks.succeedModule.tap("DemoPlugin", (module) => {
module.addDependency(new DemoDependency());
});
});
}
};示例插件的关键步骤:
- 编写
DemoDependency与DemoDependencyTemplate类,其中DemoDependencyTemplate在其apply中调用source.insert插入字符串 - 使用
compilation.dependencyTemplates注册映射关系 - 使用
thisCompilation钩子取得compilation对象 - 使用
succeedModule钩子订阅模块构建完毕事件,并调用module.addDependency方法添加依赖
webpack_require 运行时函数体系详解
为了正常运行业务项目,Webpack 需要将开发者编写的业务代码以及支撑这些业务代码的 运行时 一并打包到产物中。以建筑作类比的话,业务代码相当于砖瓦水泥,是看得见摸得着能直接感知的逻辑;运行时相当于掩埋在砖瓦之下的钢筋地基。
大多数 Webpack 特性都需要特定运行时才能跑起来,包括:异步加载、HMR、WASM、Module Federation 等。即使没有用到这些特性,仅仅是最简单的模块导入导出,也需要生成若干模拟 CMD 模块化方案的运行时代码。
webpack_require 执行流程图
核心运行时函数详解
1. installedModules 缓存对象
var __webpack_module_cache__ = {};用于存储已被引用过的模块,避免重复执行。
2. webpack_require(moduleId) 模块加载函数
function __webpack_require__(moduleId) {
// 检查缓存
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 创建新模块并存入缓存
var module = __webpack_module_cache__[moduleId] = {
id: moduleId,
loaded: false,
exports: {}
};
// 执行模块函数
__webpack_modules__[moduleId](module.exports, __webpack_require__, module);
// 标记加载完成
module.loaded = true;
// 返回导出对象
return module.exports;
}3. webpack_require.d (define property) — ESM 导出辅助
__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]
});
}
}
};用于将具名导出以 getter 形式挂载到 exports 对象上,实现 ESM 的 live binding(绑定导出)特性。
4. webpack_require.o (object property) — 属性检查
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);简洁的属性存在性检查工具函数。
5. webpack_require.n (default export) — 默认导出包装
__webpack_require__.n = (module) => {
var getter = module && module.__esModule
? () => module['default']
: () => module;
__webpack_require__.d(getter, { a: getter });
return getter;
};统一处理 ESM 和 CJS 模块的默认导出获取方式。
6. webpack_require.r (export symbol) — ESM 标识标记
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: 'Module'
});
}
Object.defineProperty(exports, '__esModule', {
value: true
});
};标记模块为 ESM 类型,设置 __esModule 标志和 Symbol.toStringTag。
7. webpack_require.t (toString value) — 命名空间创建
__webpack_require__.t = (value, mode) => {
if (mode & 1) value = this(value);
if (mode & 8) return value;
if ((mode & 4) && typeof value === 'object' && value && value.__esModule)
return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', {
enumerable: true,
value: value
});
if (mode & 2 && typeof value !== 'string')
for (var key in value)
__webpack_require__.d(ns, key => value[key]);
return ns;
};用于创建命名空间对象,支持不同的导出模式组合。
8. webpack_require.nc (nonce) — CSP nonce 支持
__webpack_require__.nc = void 0;用于 Content Security Policy 的 script nonce 支持,可在配置中设置。
9. webpack_require.dn (default name) — ⭐ v5.107 新增
// v5.107.0 新增:匿名默认导出的 .name 修复辅助函数
__webpack_require__.dn = (getters) => {
const define = (name, fn) =>
Object.defineProperty(getters, name, {
configurable: true,
enumerable: true,
get: fn
});
for (const key of Object.keys(getters)) {
const getter = getters[key];
if (typeof getter === 'function') {
// 检查是否为匿名函数或类
if (!getter.name | | getter.name === 'anonymous' | | getter.name === '') {
define(key, function() {
const result = getter.call(this);
// 根据 ES 规范设置 .name 为 "default"
if (typeof result === 'function' | | typeof result === 'class') {
Object.defineProperty(result, 'name', {
value: 'default',
configurable: true
});
}
return result;
});
}
}
}
};v5.107 改进:将重复的内联
Object.defineProperty/Object.getOwnPropertyDescriptor调用替换为单个短调用,减少输出体积。可通过module.generator.javascript.anonymousDefaultExportName选项控制行为(应用默认true,library 模式默认false)。
ESM vs CJS Runtime 对比图
详细对比表格
| 特性 | CJS-style Runtime (默认) | ESM Output (output.module: true) | |------|-------------------------|----------------------------------| | 模块加载 | __webpack_require__(id) | 原生 import/export | | 异步加载 | __webpack_require__.e(chunkId) | 动态 import() | | 包裹形式 | IIFE (立即执行函数) | ESM bundle 或无包裹 | | 导出机制 | __webpack_require__.d 定义属性 | 原生 export 语句 | | 默认导出 | __webpack_require__.n 包装 | 直接 export default | | ESM 标记 | __webpack_require__.r 设置标志 | 无需标记 | | 浏览器兼容 | ✅ 广泛兼容 | ⚠️ 需支持 <script type="module"> | | Tree Shaking | 依赖 Terser | 可利用原生引擎优化 | | 代码体积 | 含运行时开销 | 更精简(无运行时包装) |
RuntimeChunk 提取策略
Webpack 提供 optimization.runtimeChunk 配置项来控制运行时代码的提取策略:
三种策略对比
| 策略 | 配置值 | 说明 | 适用场景 | |------|--------|------|----------| | 不提取 | false 或不配置 | 运行时内联到每个 entry chunk | 单 entry 应用、开发环境 | | 单 runtime | 'single' 或 true | 所有 entry 共享一个 runtime chunk | 多 entry 应用、长期缓存优化 | | 多 runtime | 'multiple' | 每个 entry 有独立的 runtime chunk | 需要完全独立 entry 的场景 | | 自定义 | { name: 'runtime' } | 自定义 runtime chunk 名称 | 需要精细控制的场景 |
配置示例
// webpack.config.js
module.exports = {
optimization: {
runtimeChunk: {
name: (entrypoint) => `runtime-${entrypoint.name}`,
},
},
};效果:提取 runtime 后,业务代码变更不会影响 runtime chunk 的 hash,有利于长期缓存策略。
收集运行时模块
收集流程概述
早在「构建」阶段,Webpack 就已经开始持续收集运行时依赖。当所有模块处理完毕,进入 codeGeneration 函数后,Webpack 会进一步将这些依赖对象挂载到 Chunk 中。
这个过程集中在 compilation.processRuntimeRequirements 函数,函数中包含 三次循环:
第一次循环:收集模块依赖
在「模块转译主流程」中,Template.apply 函数可能修改模块的 runtimeRequirements 数组,最终形成枚举值(如 __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 子类 → 加入到 ModuleDepedencyGraph / ChunkGraph 体系的全流程。
合并最终产物
讲完单个模块转译以及运行时模块收集过程后,我们终于来到最后一步:模块合并打包。
模块合并主流程
在 compilation.codeGeneration 执行完毕后,seal 函数调用 compilation.createChunkAssets 函数,触发 renderManifest 钩子,JavascriptModulesPlugin 插件监听到这个钩子消息后开始组装 bundle:
// Webpack 5.107
// lib/Compilation.js
class Compilation {
seal() {
// 先把所有模块的代码都转译,准备好
this.codeGenerationResults = this.codeGeneration(this.modules);
// 调用 createChunkAssets
this.createChunkAssets();
}
createChunkAssets() {
// 遍历 chunks,为每个 chunk 执行 render 操作
for (const chunk of this.chunks) {
// 触发 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 钩子返回组装函数 render
const render = () =>
// render 内部根据 chunk 内容,选择使用模板 `renderMain` 或 `renderChunk`
this.renderMain(options);
result.push({ render /* arguments */ });
return result;
}
);
});
}
renderMain() {/* 渲染主 chunk */}
renderChunk() {/* 渲染子 chunk */}
}这里的核心逻辑是,compilation 以 renderManifest 钩子方式对外发布 bundle 打包需求;JavascriptModulesPlugin 监听这个钩子,按照 chunk 的内容特性,调用不同的打包函数。
💡 提示:上述仅针对 Webpack 5 有效,在 Webpack 4 中,打包逻辑集中在
MainTemplate完成。
JavascriptModulesPlugin.renderMain 函数
renderMain 函数涉及比较多场景判断,我摘了几个重点步骤:
// Webpack 5.107
// lib/javascript/JavascriptModulesPlugin.js
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 模块代码
- 返回结果
最终产物模板框架
示例右边 bundle 文件中,红框部分为用户代码文件及运行时模块生成的产物,其余部分撑起了一个 IIFE 形式的运行框架,即为模板框架:
(() => { // webpackBootstrap
"use strict";
var __webpack_modules__ = ({
"module-a": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// ! module a 代码
}),
"module-b": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// ! module b 代码
})
});
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// ! webpack CMD 实现
}
/************************************************************************/
// ! 各种 runtime helpers (.d, .o, .r, .n, .t, .nc, .dn)
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE
(() => {
// ! entry 模块代码
})();
})();运行框架包含的关键部分:
- 最外层是一个 IIFE 包裹
- 一个记录了除
entry外其它模块代码的__webpack_modules__对象 - 一个极度简化的 CMD 实现:
__webpack_require__函数 - 最后,一个包裹了
entry代码的 IIFE 函数
异步模块加载机制
Webpack 支持两种异步模块加载方式:
1. JSONP 方式(默认)
通过动态创建 <script> 标签加载远程 chunk 文件,利用 JSONP 回调机制传递模块数据。
// __webpack_require__.e 简化实现
__webpack_require__.e = (chunkId) => {
return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, []));
};2. Import Scripts 方式
通过 output.chunkLoading: 'import-scripts' 配置启用,使用 import() 加载脚本:
// webpack.config.js
module.exports = {
output: {
chunkLoading: 'import-scripts',
},
};ESM Dynamic Boundary(ESM Output 模式)
当 output.module: true 时,异步加载使用原生的动态 import() 语法,无需 JSONP 包装:
// ESM output 下的异步加载
import(/* webpackChunkName: "async-module" */ './async').then(module => {
console.log(module.default);
});优势:浏览器原生支持预加载、流式解析,性能更优。
HMR 运行时接口
热模块替换(Hot Module Replacement)需要特定的运行时支持:
核心 API
if (module.hot) {
module.hot.accept('./component', (newModule) => {
// 当 ./component 模块变化时执行
render(newModule.default);
});
module.hot.dispose((data) => {
// 模块即将被替换前执行
data.state = saveState();
});
}运行时注入
当启用 HMR 时,Webpack 会注入以下运行时代码:
__webpack_require__.hmrD— HMR 下载管理器__webpack_require__.hmrC— HMR 运行时检查webpack/runtime/hot— HMR 主模块
Scope Hoisting 对运行时的影响
Scope Hoisting(ModuleConcatenation)是 Webpack 3 引入的特性,在 Webpack 5 中通过 optimization.concatenateModules: true 启用。
性能影响
主要优势:
- 减少函数声明:多个模块合并到一个函数作用域
- 变量提升:模块级别的变量提升到共享作用域
- 消除闭包开销:无需为每个模块创建新的执行上下文
- 减小包体积:减少运行时包装代码
限制条件:
- 必须是 ES Module 格式的模块
- 不能被其他模块通过
require()导入 - 模块不能被多次引用(除非启用了特定的优化)
总结
从《Init、Make、Seal:真正读懂 Webpack 核心流程》开始,我们花了四节篇幅,终于讲完了 Webpack 构建主流程中方方面面的原理:
- Webpack 构建过程可以简单划分为 Init、Make、Seal 三个阶段
- Init 阶段负责初始化 Webpack 内部若干插件与状态
- Make 阶段解决资源读入问题,递归读入、解析所有模块内容,构建 ModuleGraph
- Seal 阶段更复杂:
- 根据 ModuleGraph 构建 ChunkGraph
- 遍历 ChunkGraph,转译每一个模块代码
- 将所有模块与模块运行时依赖合并为最终输出的 Bundle
本章重点:
__webpack_require__函数族:理解了 9 个核心运行时函数的作用和实现原理- 模块转译机制:
JavascriptGenerator+Template.apply体系的协作模式 - 运行时收集与合并:三次循环收集依赖,最终组装为完整 Bundle
- ESM vs CJS Runtime:两种输出模式的差异和适用场景
- v5.107 新增特性:
__webpack_require__.dn辅助函数优化默认导出处理
这些内容都是介绍 Webpack 实现原理的核心知识,能够帮助你:
- 分析、理解复杂开源代码的能力
- 理解 Webpack 架构及实现细节,遇到问题时能迅速定位根源
- 理解 Webpack 为 hooks、loader 提供的上下文,自如地实现自己的组件
希望你能沿着这个思路,反复、仔细阅读这些章节,深入理解底层实现原理,成为真正意义上的 Webpack 专家。
思考题
-
Dependency、Module 之间是什么关系?为什么需要设计 Dependency 这个看似可有可无的结构?
-
在什么场景下应该选择
output.module: true?它有哪些限制? -
如何优化 runtime chunk 的缓存策略?请结合 HTTP 缓存机制说明。
-
v5.107 新增的
__webpack_require__.dn函数解决了什么问题?它的设计考量是什么?