接上文:浅析 webpack 打包流程(原理) 三 - 生成 chunk
六、chunk 优化
chunk 优化阶段概述:
暴露了很多 chunk 优化相关的钩子:
触发optimize
相关 hook 移除空 chunk 和 重复 chunk,如配置了SplitChunksPlugin
也会在此时进行 chunk 分包;
然后触发其他 hook 分别设置 module.id、chunk.id 并对它们进行排序;
创建了各类 hash,包括 module hash,chunk hash,content hash,fullhash,hash。
之前 chunk 已经根据 webpack 的预处理和默认规则进行了一轮分包,现在 webpack 会根据我们配置的插件来对 chunks 进行优化。
6.1 chunk 的初步优化
在触发 compilation.hooks: optimize、optimizeModules (负责 module 相关的优化) 等之后,忽略本次打包未触发插件的钩子,执行this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups)
触发插件:
-
EnsureChunkConditionsPlugin
处理 chunkCondition -
RemoveEmptyChunksPlugin
移除空 chunk -
MergeDuplicateChunksPlugin
处理重复 chunk
this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups)
触发插件:
-
SplitChunksPlugin
优化切割 chunk,可以看下插件内compilation.hooks.optimizeChunksAdvanced.tap(...)
注册的代码 -
RemoveEmptyChunksPlugin
再次移除空 chunk -
RuntimeChunkPlugin
如有配置optimization.runtimeChunk
,可单独抽离 runtime 代码
6.2 设置 module.id
this.hooks.reviveModules.call(this.modules, this.records)
触发插件:
-
RecordIdsPlugin
设置 module.id
this.hooks.beforeModuleIds.call(this.modules)
触发插件
-
NamedModulesPlugin
设置 module.id 为 文件相对路径
然后执行:this.applyModuleIds();
这一步主要用于设置 module.id (如 id 在上一步没有设置的话),内部具体算法为:
先遍历各个 module,找出其中最大的 id 以它为最大值(usedIdmax
),计算出比它小的所有未使用的正整数和(usedIdmax + 1
)作为unusedIds
,用于给没有设置 id 的 module 使用,unusedIds
用尽后,则设置 id 为 (usedIdmax + 1) ++
this.sortItemsWithModuleIds();
:根据 module.id 给 module、chunk、reasons 等排序。
6.3 设置 chunk.id
this.hooks.reviveChunks.call(this.chunks, this.records)
触发插件
-
RecordIdsPlugin
设置 chunk.id
this.hooks.optimizeChunkOrder.call(this.chunks)
触发插件
-
OccurrenceOrderChunkIdsPlugin
chunks 排序
this.hooks.beforeChunkIds.call(this.chunks)
触发插件
-
NamedChunksPlugin
设置 chunk.id = chunk.name
this.applyChunkIds();
这一步主要用于设置 chunk.id,算法与this.applyModuleIds()
一致。
this.sortItemsWithChunkIds();
根据 chunk.id 给 module、chunk、reasons、errors、warnings、children 等排序,然后:
// /lib/Compilation.js
if (shouldRecord) {
this.hooks.recordModules.call(this.modules, this.records);
this.hooks.recordChunks.call(this.chunks, this.records);
}
依旧是对 records 的一些设置。
6.4 创建 hash
接下来执行:
// /lib/Compilation.js
this.hooks.beforeHash.call();
this.createHash();
this.hooks.afterHash.call();
if (shouldRecord) {
this.hooks.recordHash.call(this.records);
}
进入 createHash 方法,先初始化一个 hash,然后执行:
// /lib/Compilation.js
createHash() {
// ... 初始化 hash
this.mainTemplate.updateHash(hash);
this.chunkTemplate.updateHash(hash);
}
-
mainTemplate
:本意是用来渲染主 chunk (入口 chunk) 的模版,入口 chunk 默认包含 runtime (webpackBootstrap 代码)。如果通过optimization.runtimeChunk
单独把 runtime 抽取出来,那么只有 runtime chunk 应用 mainTemplate,其余都是普通 chunk。输出的文件用output.filename
定义文件名。 -
chunkTemplate
:用来渲染生成普通 chunk 的模版。默认应用于所有异步 chunk。一旦单独提取了 runtime,则除了 runtime chunk 之外的 chunk 都属于普通 chunk。若入口 chunk 拆了其余包(比如第三方插件),那么这些拆出的同步 chunk 也应用chunkTemplate
。默认根据output.filename
定义文件名,如果定义了output.chunkFilename
则以此为准。
mainTemplate 在update('maintemplate','3')
后,触发MainTemplate.hooks: hash
,执行插件 JsonpMainTemplatePlugin、WasmMainTemplatePlugin 内的订阅事件,hash.buffer 更新为 "maintemplate3jsonp6WasmMainTemplatePlugin2"。
chunkTemplate 在update('ChunkTemplate','2')
后,触发ChunkTemplate.hooks: hash
,执行插件 JsonpChunkTemplatePlugin 内的订阅事件,hash.buffer 更新为 "maintemplate3jsonp6WasmMainTemplatePlugin2ChunkTemplate2JsonpChunkTemplatePlugin4webpackJsonpwindow"。
// /lib/Compilation.js
// moduleTemplates 为 complation 实例化时所定义
this.moduleTemplates = {
javascript: new ModuleTemplate(this.runtimeTemplate, 'javascript'),
webassembly: new ModuleTemplate(this.runtimeTemplate, 'webassembly'),
};
for (const key of Object.keys(this.moduleTemplates).sort()) {
this.moduleTemplates[key].updateHash(hash);
}
将 moduleTemplates 的 key 排序后执行各自的 updateHash,hash.buffer 更新为 "maintemplate3jsonp6WasmMainTemplatePlugin2ChunkTemplate2JsonpChunkTemplatePlugin4webpackJsonpwindow1FunctionModuleTemplatePlugin21"。
然后把 children、warnings、errors 的 hash 或者 message update 进去。
6.4.1 创建 module hash
循环初始化了每个 module 的 hash,并调用了每个 module 的 updateHash:
// /lib/Compilation.js
for (let i = 0; i < modules.length; i++) {
const module = modules[i];
const moduleHash = createHash(hashFunction);
module.updateHash(moduleHash);
module.hash = /** @type {string} */ (moduleHash.digest(hashDigest));
module.renderedHash = module.hash.substr(0, hashDigestLength);
}
让我们看下 module.updateHash 方法:
// 先调用
// /lib/NormalModule.js
updateHash(hash) {
hash.update(this._buildHash); // 这里加入了 _buildHash
super.updateHash(hash);
}
// 上面 NormalModule 的 super 调用
// /lib/Module.js
updateHash(hash) {
hash.update(`${this.id}`);
hash.update(JSON.stringify(this.usedExports));
super.updateHash(hash);
}
// 上面 Module 的 super 调用
// /lib/DependenciesBlock.js
// 调用各自 dependencies、blocks、variables 的 updateHash
updateHash(hash) {
for (const dep of this.dependencies) dep.updateHash(hash);
for (const block of this.blocks) block.updateHash(hash);
for (const variable of this.variables) variable.updateHash(hash);
}
最终得到 moduleHash.buffer 形如:
"d30251197267ff9c8f1e37f43af3b15d./src/a.jsnull12,38./src/b.jsnamespace./src/b.js./src/b.jsnamespace./src/b.jsaddaddnamespacenullnull{"name":null}0./src/c.js"
"6627949a75e04e8f80d66cbf8c7c5446./src/c.jsnull12,34./src/d.jsnamespace./src/d.js./src/d.jsnamespace./src/d.jsdefaultdefaultnamespacenullnull{"name":null}./src/b.js"
......
然后生成 module 各自的 hash 和 renderedHash。
6.4.2 创建 chunk hash
继续往下,先对 chunks 进行排序,然后执行 chunks 的遍历:循环初始化每个 chunk 的 hash,并调用每个 chunk 的 updateHash。
// /lib/Compilation.js
// 遍历 chunks
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const chunkHash = createHash(hashFunction); // 初始化每个 chunk 的 hash
try {
if (outputOptions.hashSalt) {
chunkHash.update(outputOptions.hashSalt);
}
chunk.updateHash(chunkHash);
// 判断 chunk 是否含有 runtime 代码
const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate;
template.updateHashForChunk(chunkHash, chunk, this.moduleTemplates.javascript, this.dependencyTemplates);
this.hooks.chunkHash.call(chunk, chunkHash);
chunk.hash = /** @type {string} */ (chunkHash.digest(hashDigest));
hash.update(chunk.hash);
chunk.renderedHash = chunk.hash.substr(0, hashDigestLength);
this.hooks.contentHash.call(chunk);
} catch (err) {
this.errors.push(new ChunkRenderError(chunk, '', err));
}
}
chunk 的 updateHash 方法:
// /lib/Chunk.js
updateHash(hash) {
hash.update(`${this.id} `);
hash.update(this.ids ? this.ids.join(",") : "");
hash.update(`${this.name || ""} `);
for (const m of this._modules) {
hash.update(m.hash); // 把每个 module 的 hash 一并加入
}
}
得到 chunkHash.buffer,然后判断 chunk 是否含有 runtime 代码,有就使用 mainTemplate 作为模版,无就用 chunkTemplate。
runtime:字面意思是运行时代码。它主要内容是名为 webpackBootstrap 的一个自执行函数,包含模块交互时连接模块所需的加载和解析逻辑的所有代码,还伴随着 manifest 数据(chunks 映射关系的 list)。它负责项目的运行,webpack 通过它来连接模块化应用程序。
然后执行: template.updateHashForChunk:
chunkTemplate.updateHashForChunk
// /lib/ChunkTemplate.js
updateHashForChunk(hash, chunk, moduleTemplate, dependencyTemplates) {
// 与上文 Compilation 的 createHash 中 this.chunkTemplate.updateHash(hash) 执行相同
this.updateHash(hash);
this.hooks.hashForChunk.call(hash, chunk);
}
ChunkTemplate.hooks:hashForChunk
触发插件 JsonpChunkTemplatePlugin 的注册事件:update、entryModule 和 group.childrenIterable。
mainTemplate.updateHashForChunk
// /lib/MainTemplate.js
updateHashForChunk(hash, chunk, moduleTemplate, dependencyTemplates) {
// 与上文 Compilation 的 createHash 中 this.mainTemplate.updateHash(hash) 执行相同
this.updateHash(hash);
this.hooks.hashForChunk.call(hash, chunk);
for (const line of this.renderBootstrap("0000", chunk, moduleTemplate, dependencyTemplates)) {
hash.update(line);
}
}
MainTemplate.hooks:hashForChunk
触发插件 TemplatedPathPlugin 注册事件,根据 chunkFilename 的不同配置,update chunk.getChunkMaps 的不同导出。
以下为chunk.getChunkMaps 方法:
// /lib/Chunk.js
getChunkMaps(realHash) {
const chunkHashMap = Object.create(null);
const chunkContentHashMap = Object.create(null);
const chunkNameMap = Object.create(null);
for (const chunk of this.getAllAsyncChunks()) {
chunkHashMap[chunk.id] = realHash ? chunk.hash : chunk.renderedHash;
for (const key of Object.keys(chunk.contentHash)) {
if (!chunkContentHashMap[key]) {
chunkContentHashMap[key] = Object.create(null);
}
chunkContentHashMap[key][chunk.id] = chunk.contentHash[key];
}
if (chunk.name) {
chunkNameMap[chunk.id] = chunk.name;
}
}
return {
hash: chunkHashMap, // chunkFilename 配置为 chunkhash 的导出
contentHash: chunkContentHashMap, // chunkFilename 配置为 contentHash 的导出
name: chunkNameMap // chunkFilename 配置为 name 的导出
};
}
可见各种类型的 hash 都与其他不含 runtime 模块 的 hash 有强关联,所以前面给 chunk 排序也就很重要。
this.renderBootstrap 用于拼接 webpack runtime bootstrap 代码字符串。这里相当于把每一行 runtime 代码循环 update 进去,到此 chunk hash 生成结束。 将 chunk.hash update 到 hash 上。 最终得到 chunk.hash 和 chunk.renderedHash。
6.4.3 创建 content hash & fullhash & hash
接着执行:this.hooks.contentHash.call(chunk)
触发 JavascriptModulesPlugin 订阅事件,主要作用是创建生成chunk.contentHash.javascript
,也就是 contentHash 生成相关,大体跟生成 chunk hash 一致。
最后在 createHash 里得到 compilation.hash 和 compilation.fullhash,hash 生成到此结束。chunk 相关优化完成 ✅。
下文:浅析 webpack 打包流程(原理) 五 - 构建资源
webpack 打包流程系列(未完):
浅析 webpack 打包流程(原理) - 案例 demo
浅析 webpack 打包流程(原理) 一 - 准备工作
浅析 webpack 打包流程(原理) 二 - 递归构建 module
浅析 webpack 打包流程(原理) 三 - 生成 chunk
浅析 webpack 打包流程(原理) 四 - chunk 优化
浅析 webpack 打包流程(原理) 五 - 构建资源
浅析 webpack 打包流程(原理) 六 - 生成文件
参考鸣谢:
webpack打包原理 ? 看完这篇你就懂了 !
webpack 透视——提高工程化(原理篇)
webpack 透视——提高工程化(实践篇)
webpack 4 源码主流程分析
[万字总结] 一文吃透 Webpack 核心原理
有点难的 Webpack 知识点:Dependency Graph 深度解析
webpack系列之六chunk图生成
网友评论