接上篇【webpack 模塊加載原理及打包文件分析 (一)】
入口文件拆包(如拆分 runtime)的情況
webpack 的默認(rèn)處理機(jī)制是入口文件及它的同步依賴(包括第三方包)打包成一個(gè) chunk,里邊還包含 runtime (自執(zhí)行的 webpackBootstrap 函數(shù)),然后每個(gè)異步引入的模塊單獨(dú)輸出一個(gè) chunk。
關(guān)于異步模塊:1. 如果有入口 chunk 中同樣的同步依賴,最終輸出的異步 chunk 中不會(huì)包含這個(gè)依賴,而是直接引用入口中的這個(gè)模塊;2. 入口中沒(méi)有的同步依賴會(huì)被打包進(jìn)異步 chunk;3. 它的每個(gè)異步依賴或者同步的第三方包,會(huì)被單獨(dú)打包成一個(gè) chunk。
入口 chunk 即包含我們?nèi)肟谀K及其依賴的 js,是運(yùn)行時(shí)最先(初始)加載的 js。實(shí)際項(xiàng)目中,為了性能優(yōu)化減少單個(gè) js 的體積通常都會(huì)將入口 chunk 分割成幾個(gè),同時(shí)將幾乎每次打包都會(huì)變化的 runtime 單獨(dú)抽出,以保證部分 chunk 穩(wěn)定的緩存。
// webpack 配置
module.exports = {
optimization: {
runtimeChunk: { // 抽出 bootstrap 運(yùn)行代碼
name: 'runtime',
},
splitChunks: { // 優(yōu)化分包,默認(rèn)會(huì)抽出第三方包和公共模塊
chunks: 'all',
}
}
}
通過(guò)配置optimization.runtimeChunk將 runtime 抽出,原本的 webpackBootstrap IIFE函數(shù)不再像上面一樣包含在 index.js 中,而單獨(dú)成一個(gè)runtime.js。


打包結(jié)果中的 Entrypoint 會(huì)告知我們?nèi)肟谖募徊鸱殖赡男?chunk,以及這幾個(gè)js的加載順序(從前到后)。
可以通過(guò)配置 webpack-dev-server ,將項(xiàng)目運(yùn)行在瀏覽器上來(lái)觀察運(yùn)行過(guò)程。
前一篇我們講過(guò)單個(gè)入口 chunk (上篇的index.js) 執(zhí)行邏輯,簡(jiǎn)單地說(shuō)就是:將包含所有同步模塊(包括第三方包)的對(duì)象作為參數(shù)傳入 bootstrap 執(zhí)行,然后異步 chunk (上篇的0.js) 在用到時(shí)發(fā)起JSONP請(qǐng)求加載并執(zhí)行。
而現(xiàn)在不僅bootstrap中會(huì)多出不少代碼,運(yùn)行項(xiàng)目執(zhí)行的流程也會(huì)有所不同:
首先都會(huì)多生成一個(gè)變量deferredModules和一個(gè)checkDeferredModules函數(shù)。
重點(diǎn)拎出 ④deferredModules:用于緩存運(yùn)行當(dāng)前 webapp 需要的入口 module ID 以及 依賴的同步 chunk ID(截圖中 Entrypoint 指明了入口文件需后于 runtime 和 vendors~index 執(zhí)行),這個(gè)很好理解,我們自己寫(xiě)的代碼基本上都需要第三方包先加載成功后才能運(yùn)行。
而checkDeferredModules方法就是在deferredModules有數(shù)據(jù)的基礎(chǔ)上,查看運(yùn)行入口模塊之前有無(wú)其他必須先運(yùn)行的 chunk,再確認(rèn)這些 js 已經(jīng)執(zhí)行完畢再開(kāi)始同步加載入口模塊的代碼。
其余bootstrap代碼簡(jiǎn)化如下,只留本次需要用到的,略掉上篇講過(guò)的異步部分:
// 加載異步 chunk 或其他被拆分的同步 chunk 后的回調(diào)函數(shù)
// 該方法會(huì)記錄管理 chunk 的加載狀態(tài),并將 moreModules 裝載到 modules 中
// 如果是異步 chunk 會(huì)把它的 promise resolve 出去,也就是讓`_webpack_require__.e().then` 里的回調(diào)得以繼續(xù)執(zhí)行
function webpackJsonpCallback(data) {
var chunkIds = data[0]; // chunkID 數(shù)組
var moreModules = data[1]; // chunk 里所有的模塊對(duì)象
var executeModules = data[2]; // 在執(zhí)行`index.js`時(shí)即入口模塊`chunk`才會(huì)有的第三個(gè)參數(shù) [["./src/a.js","runtime","vendors~index"]]
var moduleId, chunkId, i = 0;
for(;i < chunkIds.length; i++) {
installedChunks[chunkId] = 0; // 把這個(gè) chunk 標(biāo)記為已加載
}
// 遍歷 moreModules,把 chunk 所有模塊內(nèi)容深拷貝給 modules,也就是 webpackBootstrap 的參數(shù)指向的地址
// modules 為 webpackBootstrap 的閉包變量,作用域內(nèi)的函數(shù)自然可以獲取
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
// 執(zhí)行 window["webpackJsonp"] 原生的.push,那么 webpackJsonp 數(shù)組此時(shí)就有了這個(gè) chunk 的所有信息
if(parentJsonpFunction) parentJsonpFunction(data);
// add entry modules from loaded chunk to deferred list
// 把 executeModules 的所有項(xiàng)添加到 deferredModules 數(shù)組,
// 每一是由入口模塊ID 和它依賴的 chunkID (即需要在入口模塊之前執(zhí)行的 chunk) 組成的數(shù)組,如["./src/a.js","runtime","vendors~index"]
deferredModules.push.apply(deferredModules, executeModules || []);
// run deferred modules when all chunks ready
// 運(yùn)行延遲的同步模塊
return checkDeferredModules();
}
function checkDeferredModules() {
var result;
for(var i = 0; i < deferredModules.length; i++) { // 遍歷二維數(shù)組
var deferredModule = deferredModules[i]; // 如 ["./src/a.js","runtime","vendors~index"]
var fulfilled = true;
for(var j = 1; j < deferredModule.length; j++) {
var depId = deferredModule[j]; // 入口模塊ID 或 它依賴的 chunk ID
// 確認(rèn)入口模塊依賴的每一個(gè) chunk 都加載執(zhí)行了
if(installedChunks[depId] !== 0) fulfilled = false;
}
if(fulfilled) {
// i 為 -1, 即刪除最后一項(xiàng),清空 deferredModules 數(shù)組
deferredModules.splice(i--, 1);
// deferredModule[0] 為第一項(xiàng),即入口模塊的ID'./src/a.js"',實(shí)際運(yùn)行時(shí)這個(gè) ID 是 0
result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
}
}
return result;
}
var installedChunks = {
"runtime": 0, // 初始加載的 chunk
// "index": 0 // 如沒(méi)抽 runtime
};
// 緩存延遲加載入口模塊和 chunk
// 數(shù)組的每一項(xiàng)是 一個(gè)入口模塊ID 及 它依賴的 chunk ID 組成的數(shù)組
// 二維設(shè)計(jì)是為了多入口模式
var deferredModules = [];
// 全局變量 window["webpackJsonp"],存儲(chǔ)動(dòng)態(tài)導(dǎo)入/入口拆分出的同步 chunks
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
// 定義 jsonpArray 的原生 push 方法
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 重寫(xiě) window["webpackJsonp"].push 方法為 webpackJsonpCallback 函數(shù)
jsonpArray.push = webpackJsonpCallback;
// 把 jsonpArray 還原成普通數(shù)組
jsonpArray = jsonpArray.slice();
// jsonpArray 不為空時(shí)為每項(xiàng)循環(huán)執(zhí)行 webpackJsonpCallback
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
// jsonpArray 原生 push 方法賦給 parentJsonpFunction
var parentJsonpFunction = oldJsonpFunction;
// 未抽出 runtime 且入口分包的情況
// deferredModules.push(["./src/a.js","vendors~index"]);
// return checkDeferredModules();
// run deferred modules from other chunks
// 檢查有無(wú)延遲同步塊并去運(yùn)行
checkDeferredModules();
如果runtime被抽出,webpackBootstrap 傳入的參數(shù)為一個(gè)空數(shù)組。原本的index.js中 push 的參數(shù)(Array)會(huì)有第三項(xiàng)值,結(jié)構(gòu)是一個(gè)[[入口模塊 ID, runtime, 第三方包c(diǎn)hunk ID(如果有的話)]]的二維數(shù)組:本例:[["./src/a.js","runtime","vendors~index"]],簡(jiǎn)化如下:
// dist/index.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["index"],{
"./src/a.js": (function() {}),
"./src/b.js": (function() { }),
"./src/d.js":(function() {})
},[["./src/a.js", "runtime", "vendors~index"]]]);
現(xiàn)在我們根據(jù)打包文件的執(zhí)行順序來(lái)捋一捋新增的代碼做了什么:
-
runtime.js:傳入的參數(shù)為空數(shù)組,window["webpackJsonp"]、deferredModules也是空的,除了重寫(xiě)window["webpackJsonp"].push為webpackJsonpCallback函數(shù)、做了一些變量賦值就沒(méi)有別的了。 - 然后執(zhí)行入口模塊分出的
vendors~index.js,window["webpackJsonp"].push也就webpackJsonpCallback,把這個(gè) chunk 以"vendors~index": 0的形式存儲(chǔ)到installedChunks對(duì)象,以表示這個(gè) chunk 已加載。跟著把 chunk 所有模塊內(nèi)容深拷貝給 modules。
再執(zhí)行webpackJsonp原生的push,把vendors~indexchunk 的所有信息存入window["webpackJsonp"]數(shù)組,信息為一個(gè)包含兩項(xiàng)內(nèi)容的數(shù)組:[["vendors~index"], 包含文件中所有 modules 的對(duì)象]。 - 再依法炮制
index.js,"index": 0存儲(chǔ)到installedChunks,把 chunk 所有模塊內(nèi)容深拷貝給 modules。再把chunk 的所有信息存入全局"webpackJsonp",信息為包含三項(xiàng)內(nèi)容的數(shù)組:[["index"], 包含入口模塊和它的同步依賴信息的 modules 對(duì)象,[["./src/a.js","runtime","vendors~index"]]]。
跟著把這第三個(gè)參數(shù)添加到deferredModules數(shù)組,再通過(guò)checkDeferredModules遍歷這個(gè)數(shù)組,確認(rèn)入口模塊的同步依賴都已經(jīng)加載后,用__webpack_require__去執(zhí)行入口模塊。(注:開(kāi)發(fā)環(huán)境模塊 ID 是源文件路徑,生產(chǎn)環(huán)境則是一些數(shù)字或字符串標(biāo)識(shí),例如0) - 此時(shí)所有同步模塊的數(shù)據(jù)都以 { 模塊ID: 模塊函數(shù), ... } 的形式存儲(chǔ)在 webpackBootstrap 的
modules閉包變量中,因此通過(guò)執(zhí)行入口模塊(a.js)的函數(shù),連接其他同步模塊時(shí)都可以通過(guò) module ID 獲取并執(zhí)行它們的模塊函數(shù)。如果是這些模塊已經(jīng)被執(zhí)行過(guò),會(huì)被存在installedModules里,需要引用時(shí)直接獲取模塊導(dǎo)出值(exports)即可。(詳見(jiàn)上一篇的__webpack_require__部分,執(zhí)行的模塊函數(shù)都是通過(guò)modules[moduleId]拿到的) - 之后異步模塊的部分,上篇已講過(guò)不再贅述。
若 script 標(biāo)簽加上 async 屬性
入口拆包后的加載機(jī)制其實(shí)很簡(jiǎn)單,一言蔽之就是在加載入口模塊之前把同步依賴的 chunk 都先執(zhí)行了,然后執(zhí)行入口 module 代碼。
webpackBootstrap 代碼設(shè)計(jì)得很巧妙,拆包后的同步 chunk 即使不是按照本該的順序執(zhí)行,項(xiàng)目也能正常運(yùn)行。
比如先于runtime運(yùn)行了vendors~index,那么window["webpackJsonp"]就已經(jīng)包含了這個(gè) chunk 的信息,然后再執(zhí)行 bootstrap,改寫(xiě) webpackJsonp 的原生 push 為 webpackJsonpCallback。
為webpackJsonp數(shù)組每一項(xiàng) (chunk) 執(zhí)行webpackJsonpCallback。
于是每次加載完當(dāng)前 chunk 都會(huì)調(diào)用checkDeferredModules判斷是否它是否有依賴的 chunk,有的話保證這些 chunk 加載完畢后就會(huì)去執(zhí)行入口 module。
借用一張大神的圖大致說(shuō)明以上兩種情況的流程:

??:標(biāo)注 ①、②、③、④ 的四個(gè)變量需要重點(diǎn)理解,對(duì)理解 webpack 加載邏輯很有幫助。
參考文章:
Webpack 是怎樣運(yùn)行的?(一)
Webpack 是怎樣運(yùn)行的?(二)
聊聊 webpack 異步加載(一):webpack 如何加載拆包后的代碼
webpack是如何實(shí)現(xiàn)動(dòng)態(tài)導(dǎo)入的