接上文:淺析 webpack 打包流程(原理) 三 - 生成 chunk
六煞抬、chunk 優(yōu)化
chunk 優(yōu)化階段概述:
暴露了很多 chunk 優(yōu)化相關(guān)的鉤子:
觸發(fā)optimize
相關(guān) hook 移除空 chunk 和 重復(fù) chunk琳状,如配置了SplitChunksPlugin
也會在此時進(jìn)行 chunk 分包;
然后觸發(fā)其他 hook 分別設(shè)置 module.id、chunk.id 并對它們進(jìn)行排序甸陌;
創(chuàng)建了各類 hash,包括 module hash钟沛,chunk hash券腔,content hash伏穆,fullhash,hash纷纫。
之前 chunk 已經(jīng)根據(jù) webpack 的預(yù)處理和默認(rèn)規(guī)則進(jìn)行了一輪分包枕扫,現(xiàn)在 webpack 會根據(jù)我們配置的插件來對 chunks 進(jìn)行優(yōu)化。
6.1 chunk 的初步優(yōu)化
在觸發(fā) compilation.hooks: optimize辱魁、optimizeModules (負(fù)責(zé) module 相關(guān)的優(yōu)化) 等之后烟瞧,忽略本次打包未觸發(fā)插件的鉤子,執(zhí)行this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups)
觸發(fā)插件:
-
EnsureChunkConditionsPlugin
處理 chunkCondition -
RemoveEmptyChunksPlugin
移除空 chunk -
MergeDuplicateChunksPlugin
處理重復(fù) chunk
this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups)
觸發(fā)插件:
-
SplitChunksPlugin
優(yōu)化切割 chunk染簇,可以看下插件內(nèi)compilation.hooks.optimizeChunksAdvanced.tap(...)
注冊的代碼 -
RemoveEmptyChunksPlugin
再次移除空 chunk -
RuntimeChunkPlugin
如有配置optimization.runtimeChunk
参滴,可單獨抽離 runtime 代碼
6.2 設(shè)置 module.id
this.hooks.reviveModules.call(this.modules, this.records)
觸發(fā)插件:
-
RecordIdsPlugin
設(shè)置 module.id
this.hooks.beforeModuleIds.call(this.modules)
觸發(fā)插件
-
NamedModulesPlugin
設(shè)置 module.id 為 文件相對路徑
然后執(zhí)行:this.applyModuleIds();
這一步主要用于設(shè)置 module.id (如 id 在上一步?jīng)]有設(shè)置的話),內(nèi)部具體算法為:
先遍歷各個 module剖笙,找出其中最大的 id 以它為最大值(usedIdmax
)卵洗,計算出比它小的所有未使用的正整數(shù)和(usedIdmax + 1
)作為unusedIds
,用于給沒有設(shè)置 id 的 module 使用弥咪,unusedIds
用盡后过蹂,則設(shè)置 id 為 (usedIdmax + 1) ++
this.sortItemsWithModuleIds();
:根據(jù) module.id 給 module、chunk聚至、reasons 等排序酷勺。
6.3 設(shè)置 chunk.id
this.hooks.reviveChunks.call(this.chunks, this.records)
觸發(fā)插件
-
RecordIdsPlugin
設(shè)置 chunk.id
this.hooks.optimizeChunkOrder.call(this.chunks)
觸發(fā)插件
-
OccurrenceOrderChunkIdsPlugin
chunks 排序
this.hooks.beforeChunkIds.call(this.chunks)
觸發(fā)插件
-
NamedChunksPlugin
設(shè)置 chunk.id = chunk.name
this.applyChunkIds();
這一步主要用于設(shè)置 chunk.id,算法與this.applyModuleIds()
一致扳躬。
this.sortItemsWithChunkIds();
根據(jù) 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 的一些設(shè)置偶摔。
6.4 創(chuàng)建 hash
接下來執(zhí)行:
// /lib/Compilation.js
this.hooks.beforeHash.call();
this.createHash();
this.hooks.afterHash.call();
if (shouldRecord) {
this.hooks.recordHash.call(this.records);
}
進(jìn)入 createHash 方法,先初始化一個 hash促脉,然后執(zhí)行:
// /lib/Compilation.js
createHash() {
// ... 初始化 hash
this.mainTemplate.updateHash(hash);
this.chunkTemplate.updateHash(hash);
}
-
mainTemplate
:本意是用來渲染主 chunk (入口 chunk) 的模版辰斋,入口 chunk 默認(rèn)包含 runtime (webpackBootstrap 代碼)。如果通過optimization.runtimeChunk
單獨把 runtime 抽取出來瘸味,那么只有 runtime chunk 應(yīng)用 mainTemplate宫仗,其余都是普通 chunk。輸出的文件用output.filename
定義文件名旁仿。 -
chunkTemplate
:用來渲染生成普通 chunk 的模版藕夫。默認(rèn)應(yīng)用于所有異步 chunk。一旦單獨提取了 runtime,則除了 runtime chunk 之外的 chunk 都屬于普通 chunk毅贮。若入口 chunk 拆了其余包(比如第三方插件)梭姓,那么這些拆出的同步 chunk 也應(yīng)用chunkTemplate
。默認(rèn)根據(jù)output.filename
定義文件名嫩码,如果定義了output.chunkFilename
則以此為準(zhǔn)誉尖。
mainTemplate 在update('maintemplate','3')
后,觸發(fā)MainTemplate.hooks: hash
铸题,執(zhí)行插件 JsonpMainTemplatePlugin铡恕、WasmMainTemplatePlugin 內(nèi)的訂閱事件,hash.buffer 更新為 "maintemplate3jsonp6WasmMainTemplatePlugin2"丢间。
chunkTemplate 在update('ChunkTemplate','2')
后探熔,觸發(fā)ChunkTemplate.hooks: hash
,執(zhí)行插件 JsonpChunkTemplatePlugin 內(nèi)的訂閱事件烘挫,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 排序后執(zhí)行各自的 updateHash,hash.buffer 更新為 "maintemplate3jsonp6WasmMainTemplatePlugin2ChunkTemplate2JsonpChunkTemplatePlugin4webpackJsonpwindow1FunctionModuleTemplatePlugin21"饮六。
然后把 children其垄、warnings、errors 的 hash 或者 message update 進(jìn)去卤橄。
6.4.1 創(chuàng)建 module hash
循環(huán)初始化了每個 module 的 hash绿满,并調(diào)用了每個 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 方法:
// 先調(diào)用
// /lib/NormalModule.js
updateHash(hash) {
hash.update(this._buildHash); // 這里加入了 _buildHash
super.updateHash(hash);
}
// 上面 NormalModule 的 super 調(diào)用
// /lib/Module.js
updateHash(hash) {
hash.update(`${this.id}`);
hash.update(JSON.stringify(this.usedExports));
super.updateHash(hash);
}
// 上面 Module 的 super 調(diào)用
// /lib/DependenciesBlock.js
// 調(diào)用各自 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 創(chuàng)建 chunk hash
繼續(xù)往下,先對 chunks 進(jìn)行排序嚎货,然后執(zhí)行 chunks 的遍歷:循環(huán)初始化每個 chunk 的 hash橘霎,并調(diào)用每個 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:字面意思是運行時代碼七蜘。它主要內(nèi)容是名為 webpackBootstrap 的一個自執(zhí)行函數(shù)谭溉,包含模塊交互時連接模塊所需的加載和解析邏輯的所有代碼墙懂,還伴隨著 manifest 數(shù)據(jù)(chunks 映射關(guān)系的 list)。它負(fù)責(zé)項目的運行扮念,webpack 通過它來連接模塊化應(yīng)用程序损搬。
然后執(zhí)行: template.updateHashForChunk:
chunkTemplate.updateHashForChunk
// /lib/ChunkTemplate.js
updateHashForChunk(hash, chunk, moduleTemplate, dependencyTemplates) {
// 與上文 Compilation 的 createHash 中 this.chunkTemplate.updateHash(hash) 執(zhí)行相同
this.updateHash(hash);
this.hooks.hashForChunk.call(hash, chunk);
}
ChunkTemplate.hooks:hashForChunk
觸發(fā)插件 JsonpChunkTemplatePlugin 的注冊事件:update、entryModule 和 group.childrenIterable。
mainTemplate.updateHashForChunk
// /lib/MainTemplate.js
updateHashForChunk(hash, chunk, moduleTemplate, dependencyTemplates) {
// 與上文 Compilation 的 createHash 中 this.mainTemplate.updateHash(hash) 執(zhí)行相同
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
觸發(fā)插件 TemplatedPathPlugin 注冊事件巧勤,根據(jù) chunkFilename 的不同配置嵌灰,update chunk.getChunkMaps 的不同導(dǎo)出。
以下為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 的導(dǎo)出
contentHash: chunkContentHashMap, // chunkFilename 配置為 contentHash 的導(dǎo)出
name: chunkNameMap // chunkFilename 配置為 name 的導(dǎo)出
};
}
可見各種類型的 hash 都與其他不含 runtime 模塊 的 hash 有強關(guān)聯(lián)颅悉,所以前面給 chunk 排序也就很重要沽瞭。
this.renderBootstrap 用于拼接 webpack runtime bootstrap 代碼字符串。這里相當(dāng)于把每一行 runtime 代碼循環(huán) update 進(jìn)去剩瓶,到此 chunk hash 生成結(jié)束驹溃。 將 chunk.hash update 到 hash 上。 最終得到 chunk.hash 和 chunk.renderedHash延曙。
6.4.3 創(chuàng)建 content hash & fullhash & hash
接著執(zhí)行:this.hooks.contentHash.call(chunk)
觸發(fā) JavascriptModulesPlugin 訂閱事件豌鹤,主要作用是創(chuàng)建生成chunk.contentHash.javascript
,也就是 contentHash 生成相關(guān)枝缔,大體跟生成 chunk hash 一致布疙。
最后在 createHash 里得到 compilation.hash 和 compilation.fullhash,hash 生成到此結(jié)束愿卸。chunk 相關(guān)優(yōu)化完成 ?灵临。
下文:淺析 webpack 打包流程(原理) 五 - 構(gòu)建資源
webpack 打包流程系列(未完):
淺析 webpack 打包流程(原理) - 案例 demo
淺析 webpack 打包流程(原理) 一 - 準(zhǔn)備工作
淺析 webpack 打包流程(原理) 二 - 遞歸構(gòu)建 module
淺析 webpack 打包流程(原理) 三 - 生成 chunk
淺析 webpack 打包流程(原理) 四 - chunk 優(yōu)化
淺析 webpack 打包流程(原理) 五 - 構(gòu)建資源
淺析 webpack 打包流程(原理) 六 - 生成文件
參考鳴謝:
webpack打包原理 ? 看完這篇你就懂了 !
webpack 透視——提高工程化(原理篇)
webpack 透視——提高工程化(實踐篇)
webpack 4 源碼主流程分析
[萬字總結(jié)] 一文吃透 Webpack 核心原理
有點難的 Webpack 知識點:Dependency Graph 深度解析
webpack系列之六chunk圖生成