淺析 webpack 打包流程(原理) 四 - chunk 優(yōu)化

接上文:淺析 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)誉尖。

mainTemplateupdate('maintemplate','3')后,觸發(fā)MainTemplate.hooks: hash铸题,執(zhí)行插件 JsonpMainTemplatePlugin铡恕、WasmMainTemplatePlugin 內(nèi)的訂閱事件,hash.buffer 更新為 "maintemplate3jsonp6WasmMainTemplatePlugin2"丢间。
chunkTemplateupdate('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圖生成

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市趴荸,隨后出現(xiàn)的幾起案子俱诸,更是在濱河造成了極大的恐慌,老刑警劉巖赊舶,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件睁搭,死亡現(xiàn)場離奇詭異,居然都是意外死亡笼平,警方通過查閱死者的電腦和手機园骆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寓调,“玉大人锌唾,你說我怎么就攤上這事《嵊ⅲ” “怎么了晌涕?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長痛悯。 經(jīng)常有香客問我余黎,道長,這世上最難降的妖魔是什么载萌? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任惧财,我火速辦了婚禮巡扇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘垮衷。我一直安慰自己厅翔,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布搀突。 她就那樣靜靜地躺著刀闷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪仰迁。 梳的紋絲不亂的頭發(fā)上涩赢,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天,我揣著相機與錄音轩勘,去河邊找鬼筒扒。 笑死,一個胖子當(dāng)著我的面吹牛绊寻,可吹牛的內(nèi)容都是我干的花墩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼澄步,長吁一口氣:“原來是場噩夢啊……” “哼冰蘑!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起村缸,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤祠肥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后梯皿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體仇箱,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年东羹,在試婚紗的時候發(fā)現(xiàn)自己被綠了剂桥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡属提,死狀恐怖权逗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情冤议,我是刑警寧澤斟薇,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站恕酸,受9級特大地震影響堪滨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜尸疆,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一椿猎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧寿弱,春花似錦犯眠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至噪矛,卻和暖如春量蕊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背艇挨。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工残炮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缩滨。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓势就,卻偏偏與公主長得像,于是被迫代替她去往敵國和親脉漏。 傳聞我的和親對象是個殘疾皇子苞冯,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,585評論 2 359

推薦閱讀更多精彩內(nèi)容