接上文:浅析 webpack 打包流程(原理) 四 - chunk 优化
七、构建资源
本阶段概述:
1.获取 compilation 每个 module 的 buildInfo.assets,然后调用 this.emitAsset 生成 module 资源;
2.遍历compilation.chunks
生成 chunk 资源时,先根据是否含有 runtime (webpackBootstrap 代码) 选择不同的 template (有则 mainTemplate,不然一概 chunkTemplate),得到各自的 manifest 数据 和 pathAndInfo,然后调用不同的 render 渲染代码;
3.最后建立文件名与资源之间的映射,并将得到的所有目标资源信息挂载到compilation.assets
上。
4.如果有配置诸如terser-webpack-plugin
的代码压缩插件(一般都有),在optimizeChunkAssets
钩子触发后,对生成的资源根据seal
阶段(生成chunk之前)做的标记进行 treeshaking。
7.1 生成 module 资源
继续执行:
// /lib/Compilation.js
this.hooks.beforeModuleAssets.call();
this.createModuleAssets();
createModuleAssets 方法获取每个 module 的 buildInfo.assets,然后触发compilation.emitAsset
生成 module 资源,得到的相关数据存储在compilation
的assets
和assetsInfo
。buildInfo.assets 相关数据可在 loader 里调用 Api: this.emitFile 来生成 (loaderContext.emitFile 方法,详见/lib/NormalModule.js
)。
7.2 生成 chunk 资源
7.2.1 生成前的准备
得到 manifest 数据对象
当 compiler 处理、解析和映射应用代码时,manifest 会记录每个模块的详细要点(如 module identifier、路径等)。程序运行时,runtime 会根据 manifest 中的数据来解析和加载模块:
__webpack_require__
方法接收模块标识符(identifier)作为参数,简称 moduleId,通过这个标识符可以检索出 manifest 集合中对应的模块。
// /lib/Compilation.js
this.hooks.beforeChunkAssets.call();
this.createChunkAssets();
createChunkAssets 方法循环对每个 chunk 执行:
// /lib/Compilation.js
createChunkAssets() {
// ...
for (let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i];
// 判断 chunk 是否包含 runtime 代码,获取到对应的 template
const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate;
// 得到相应的 manifest 数据对象
const manifest = template.getRenderManifest({
chunk,
hash: this.hash,
fullHash: this.fullHash,
outputOptions,
moduleTemplates: this.moduleTemplates,
dependencyTemplates: this.dependencyTemplates,
}); // manifest 为 `render` 所需要的全部信息:`[{ render(), filenameTemplate, pathOptions, identifier, hash }]`
// ...
}
}
判断 chunk 是否含有 runtime 代码 (它所属的 chunkGroup 是否是初始的那个 EntryPoint,chunkGroup.runtimeChunk 是否就是当前 chunk),从而获取到对应的 template:默认情况下包含 runtime 和同步依赖的 入口 chunk 对应 mainTemplate;异步 chunk 对应 chunkTemplate。
默认配置下,即未手动抽离 runtime 和 配置 splitChunks,同步依赖都会被合并在入口 chunk 中,并且从属于一个 EntryPoint(继承自chunkGroup)。而每个异步模块都会单独生成一个 chunk 和 chunkGroup。
一旦从入口 chunk 单独抽出 runtime chunk,则只有 runtime chunk 由mainTemplate
渲染,其余都属于由chunkTemplate
渲染的普通 chunk 了(无论同步异步)。
然后执行对应的 getRenderManifest,触发template.hooks:renderManifest
,执行插件 JavascriptModulesPlugin 相关事件:
(如果有配置 MiniCssExtractPlugin 插件,也会在此时执行从当前 chunk 分离出所有 CssModule [thisCompilation
时生成],并在当前 chunk 清单文件中添加一个单独的 css 文件,即抽离 css 样式到单独的*.css
)
// /lib/JavascriptModulesPlugin.js
// 运行时 chunk (默认是入口 chunk) 相关的插件事件
compilation.mainTemplate.hooks.renderManifest.tap(...)
// 普通 chunk 相关的插件事件
compilation.chunkTemplate.hooks.renderManifest.tap(
"JavascriptModulesPlugin",
(result, options) => {
// ...
result.push({
render: () =>
this.renderJavascript(
compilation.chunkTemplate,
chunk,
moduleTemplates.javascript,
dependencyTemplates
),
filenameTemplate,
pathOptions: {
chunk,
contentHashType: "javascript"
},
identifier: `chunk${chunk.id}`,
hash: chunk.hash
});
return result;
}
);
得到 manifest 即 render 所需要的全部信息:[{ render(), filenameTemplate, pathOptions, identifier, hash }]
。
如果是 chunkTemplate 还会触发插件 WebAssemblyModulesPlugin 去处理 WebAssembly 相关。
得到 pathAndInfo
然后遍历 manifest 数组,在里面执行:
// /lib/Compilation.js
const pathAndInfo = this.getPathWithInfo(
filenameTemplate,
fileManifest.pathOptions
);
getPathWithInfo 用于得到路径和相关信息,会触发mainTemplate.hooks: assetPath
,去执行插件 TemplatedPathPlugin 相关事件,使用若干 replace 将如[name].[chunkhash:8].js
替换为0.c7687fbe.js
。
7.2.2 构建资源
然后判断有无 source 缓存,若无则执行:source = fileManifest.render();
即执行 manifest 每一项里的 render 函数。
(1) chunkTemplate
生成主体 chunk 代码
如果是普通 chunk,render 会调用 JavascriptModulesPlugin.js 插件里的renderJavascript
方法:先执行 Template.renderChunkModules 静态方法:
// /lib/JavascriptModulesPlugin.js
renderJavascript(chunkTemplate, chunk, moduleTemplate, dependencyTemplates) {
// 生成每个 module 代码
const moduleSources = Template.renderChunkModules(
chunk,
m => typeof m.source === "function",
moduleTemplate,
dependencyTemplates
);
}
renderChunkModules 生成每个 module 代码
// /lib/Template.js
static renderChunkModules(chunk, filterFn, moduleTemplate, dependencyTemplates, prefix = "" ) {
const allModules = modules.map((module) => {
return {
id: module.id,
// ModuleTemplate.js 的 render 方法,循环对每一个 module 执行 render
source: moduleTemplate.render(module, dependencyTemplates, { chunk }),
};
});
}
// /lib/ModuleTemplate.js 的 render 方法
const moduleSource = module.source(
dependencyTemplates,
this.runtimeTemplate,
this.type
);
module.source 即/lib/NormalModule.js
的 source 方法,内部执行:const source = this.generator.generate(this, dependencyTemplates, runtimeTemplate, type);
这个 generator 就是在 reslove 流程 ➡️ getGenerator 所获得,即在/lib/JavascriptGenerator.js
执行:
this.sourceBlock(module, module, [], dependencyTemplates, source, runtimeTemplate);
这里循环处理 module 的每个依赖(module.dependencies),获得依赖所对应的 template 模板类,然后执行该类的 apply:
// /lib/JavascriptGenerator.js
const template = dependencyTemplates.get(dependency.constructor);
template.apply(dependency, source, runtimeTemplate, dependencyTemplates);
这里的 dependencyTemplates 就是在【浅析 webpack 打包流程(原理) 一 - 准备工作】 ➡️ 实例化 compilation 时添加的依赖模板模块。
template.apply
会根据依赖的不同做相应的源码转化处理。但方法里并没有直接执行源码转化,而是将其转化对象 push 到ReplaceSource.replacements
里,转化对象的格式为:
注:webpack-sources 提供若干类型的 source 类,如 CachedSource、PrefixSource、ConcatSource、ReplaceSource 等。它们可以组合使用,方便对代码进行添加、替换、连接等操作。同时又含有一些 source-map 相关、updateHash 等 Api 供 webpack 内部调用。
// Replacement
{
"content": "__webpack_require__.r(__webpack_exports__);\n", // 替换的内容
"end": -11, // 替换源码的终止位置
"insertIndex": 0, // 优先级
"name": undefined, // 名称
"start": -10 // 替换源码的起始位置
}
各模版的转化处理见【浅析 webpack 打包流程(原理) 二 - 递归构建 module】 最末:各依赖作用简单解释。
包裹代码
收集完依赖相关的转化对象 Replacement 后,对得到的结果进行cachedSource
缓存包装,回到 ModuleTemplate.js 的 render 方法得到 moduleSource。
然后触发ModuleTemplate.hooks:content、module、render、package
,content、module 钩子主要是可以让我们完成对 module 源码的再次处理;然后在 render 钩子里执行插件 FunctionModuleTemplatePlugin 的订阅事件:对处理后的 module 源码进行包裹,即生成代码:
/***/
(function (module, __webpack_exports__, __webpack_require__) {
'use strict';
// children 数组中的 CachedSource 即为`module`源码,里面包含 replacements
/***/
});
添加注释
再触发 package 钩子执行插件 FunctionModuleTemplatePlugin 的订阅事件,主要是添加相关注释,即生成代码:
/*!***************************************************************!*\
!*** ./src/c.js ***!
\***************************************************************/
/*! exports provided: sub */
/***/
(function (module, __webpack_exports__, __webpack_require__) {
'use strict';
// children 数组中的 CachedSource 即为`module`源码,里面包含 replacements
/***/
});
将所有的 module 都处理完毕后,回到 Template.js 的 renderChunkModules 继续处理生成代码,最终将每个 module 生成的代码串起来回到 JavascriptModulesPlugin.js 的 renderJavascript 方法里得到了 moduleSources。
生成 jsonp 包裹代码
接着触发chunkTemplate.hooks: modules
,为修改生成的 chunk 代码提供钩子。得到 core 后,触发chunkTemplate.hooks: render
执行插件 JsonpChunkTemplatePlugin.js 订阅事件,主要是添加 jsonp 包裹代码,得到:
美化一下就和我们常见的打包文件无异了:
(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
[0],
{
// 前面生成的 chunk 代码
/***/ "./src/c.js":
/*!******************!*\
!*** ./src/c.js ***!
\******************/
/*! exports provided: sub */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
/***/ })
}
]);
最后 return 一个new ConcatSource(source, ";")
。至此普通 (非初始) chunk 代码 chunkTemplate 的fileManifest.render
(/lib/Compilation.js 中) 构建完成。
注意:非初始不代表异步,当入口 chunk 被拆成多个同步 chunk,初始 chunk 就指代包含 runtime 的那个 chunk。
可以配合【webpack 模块加载原理及打包文件分析 (一)】服用,便能清晰知晓window['webpackJsonp'].push
不光用来加载异步 chunk。
(2) mainTemplate
如果是初始 chunk,render 会执行 JavascriptModulesPlugin.js 里的 compilation.mainTemplate.render 即/lib/MainTemplate.js
的 render 方法。
生成 runtime 代码
内部执行:const buf = this.renderBootstrap(hash, chunk, moduleTemplate, dependencyTemplates);
得到 webpack runtime bootstrap 代码数组,过程中会判断 chunks 中是否存在异步 chunk,如果有,则代码里还会包含异步相关的 runtime 代码。如果还有延迟加载的同步 chunk,都会在这里处理为相应的 runtime。
包裹 runtime 与 chunk 代码
然后执行:
let source = this.hooks.render.call(
new OriginalSource(
Template.prefix(buf, " \t") + "\n",
"webpack/bootstrap"
),
chunk,
hash,
moduleTemplate,
dependencyTemplates
);
先通过 Template.js 的 prefix 方法合并 runtime 代码字符串,得到 OriginalSource 实例,然后将其作为参数执行MainTemplate.hooks: render
,该 hook 事件注册在 MainTemplate 自身的 constructor 中,代码如下:
const source = new ConcatSource();
source.add('/******/ (function(modules) { // webpackBootstrap\n');
source.add(new PrefixSource('/******/', bootstrapSource));
source.add('/******/ })\n');
source.add('/************************************************************************/\n');
source.add('/******/ (');
source.add(this.hooks.modules.call(new RawSource(''), chunk, hash, moduleTemplate, dependencyTemplates));
source.add(')');
return source;
对 runtime bootstrap 代码进行了包装 (bootstrapSource 即前面生成的 runtime 代码),过程中触发MainTemplate.hooks: modules
得到 chunk 的生成代码,即最终返回一个包含 runtime 代码和 chunk 代码的 ConcatSource 实例。
生成 chunk 代码
这里来看通过this.hooks.modules.call()
钩子得到 chunk 生成代码的实现:
触发插件 JavascriptModulesPlugin 的注册事件,即执行 Template 类的静态方法renderChunkModules
。与前文 chunkTemplate ➡️ 生成主体 chunk 代码的实现一致。
最终经过包裹后得到的代码大致如下:
"/******/ (function(modules) { // webpackBootstrap
// runtime 代码的 PrefixSource 实例
/******/ })
/************************************************************************/
/******/ ({
/***/ "./src/a.js":
/*!******************!*\
!*** ./src/a.js ***!
\******************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// module a 的 CachedSource 实例
/***/ }),
/***/ "./src/b.js":
/*!******************!*\
!*** ./src/b.js ***!
\******************/
/*! exports provided: add, unusedAdd */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// module b 的 CachedSource 实例
/***/ }),
/***/ "./src/d.js":
/*!******************!*\
!*** ./src/d.js ***!
\******************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// module d 的 CachedSource 实例
/***/ })
/******/ })"
最后返回一个new ConcatSource(source, ";")
。至此入口 chunk (包含 runtime 的 chunk) 代码 mainTemplate 的fileManifest.render
(/lib/Compilation.js 中) 构建完成。
7.2.3 文件名映射资源
接下来,无论是包含 runtime 的主 chunk 还是普通 chunk,都回到 Compilation.js 的 createChunkAssets 方法,在compilation.cache[cacheName]
做了 source 缓存,然后执行:
this.emitAsset(file, source, assetInfo);
chunk.files.push(file);
建立起文件名与对应源码的联系,以this.assets[file] = source
的形式将该映射对象挂载到 compilation.assets 下。 把文件名称存入对应的 chunk.files 数组中,即compilation.chunks
下。然后设置了 alreadyWrittenFiles (Map 对象),以防重复构建代码。至此一个 chunk 的资源构建结束。
所有 chunk 遍历结束后,得到的compilation.assets
和 compilation.assetsInfo
:
// compilation
{
//...
"assets": {
"0.92bfd615.js": CachedSource, // CachedSource 里包含 chunk 资源
"index.1d678a.js": CachedSource
},
"assetsInfo": { // Map结构
0: {
"key": '0.92bfd615.js',
"value": {
immutable:true
}
},
1: {
"key": 'index.1d678a.js',
"value": {
immutable:true
}
}
}
}
7.3 对生成资源进行 TreeShaking (需配置相应插件)
在compilation.hooks.additionalAssets
钩子触发后,如果有配置进一步处理生成资源的插件,则会对资源再度优化。
比如compilation.hooks.optimizeChunkAssets
会触发terser-webpack-plugin
代码压缩插件(一般都会配置,webpack 5 内置),对生成的 chunk 资源根据(seal
阶段compilation.hooks.optimizeDependencies
)生成的unused harmony export
标记等信息进行 treeshaking。
下文:浅析 webpack 打包流程(原理) 六 - 生成文件
webpack 打包流程系列(未完):
浅析 webpack 打包流程(原理) - 案例 demo
浅析 webpack 打包流程(原理) 一 - 准备工作
浅析 webpack 打包流程(原理) 二 - 递归构建 module
浅析 webpack 打包流程(原理) 三 - 生成 chunk
浅析 webpack 打包流程(原理) 四 - chunk 优化
浅析 webpack 打包流程(原理) 五 - 构建资源
浅析 webpack 打包流程(原理) 六 - 生成文件
参考鸣谢:
webpack打包原理 ? 看完这篇你就懂了 !
webpack 透视——提高工程化(原理篇)
webpack 透视——提高工程化(实践篇)
webpack 4 源码主流程分析
[万字总结] 一文吃透 Webpack 核心原理
关于Webpack详述系列文章 (第三篇)