module、chunk、bundle 的概念
webpack 術(shù)語(yǔ)表 中有名詞解釋:
- Module: Module 是離散功能塊,相比于完整程序提供了更小的接觸面。精心編寫的模塊提供了可靠的抽象和封裝界限,使得應(yīng)用程序中每個(gè)模塊都具有條理清楚的設(shè)計(jì)和明確的目的。
- Chunk: 此 webpack 特定術(shù)語(yǔ)在內(nèi)部用于管理捆綁過(guò)程趁桃。輸出束(bundle)由 chunk 組成,其中有幾種類型(例如 entry 和 child )肄鸽。通常卫病,chunk 直接與 bundle 相對(duì)應(yīng),但是有些配置不會(huì)產(chǎn)生一對(duì)一的關(guān)系典徘。
- Bundle:由許多不同的模塊生成蟀苛,包含已經(jīng)經(jīng)過(guò)加載和編譯過(guò)程的源文件的最終版本。
- Entry Point: 入口起點(diǎn)告訴 webpack 從哪里開始逮诲,并遵循著依賴圖知道要打包哪些文件帜平∮母妫可以將應(yīng)用程序的入口起點(diǎn)視為要捆綁的內(nèi)容的 根上下文。
以我自身的理解來(lái)闡述:
在模塊化編程中裆甩,對(duì)于module(模塊)廣義的認(rèn)知是所有通過(guò)import/require
等方式引入的代碼(*.mjs
冗锁、*.js
文件)。而在萬(wàn)物皆模塊的 webpack嗤栓,項(xiàng)目中使用的任何一個(gè)資源(如css冻河、圖片)也都被視作模塊來(lái)處理。在 webpack 的編譯過(guò)程中茉帅,module 的角色是資源的映射對(duì)象叨叙,儲(chǔ)存了當(dāng)前文件所有信息,包含資源的路徑堪澎、上下文擂错、依賴、內(nèi)容等樱蛤。
原始的資源模塊以 Module 對(duì)象形式存在钮呀、流轉(zhuǎn)、解析處理刹悴。
chunk(代碼塊)是一些模塊 (module) 的封裝單元行楞。于 webpack 運(yùn)行時(shí)的 seal 封包階段生成攒暇,且直到資源構(gòu)建階段都會(huì)持續(xù)發(fā)生變化的代碼塊土匀,在此期間插件通過(guò)各種鉤子事件侵入各個(gè)編譯階段對(duì) chunk 進(jìn)行優(yōu)化處理。
webpack 在 make 階段解析所有模塊資源形用,構(gòu)建出完整的 Dependency Graph (從 Entry 入口起點(diǎn)開始遞歸收集所有模塊之間的依賴關(guān)系)就轧。然后在 seal 階段會(huì)根據(jù)配置及模塊依賴圖內(nèi)容構(gòu)建出一個(gè)或多個(gè) chunk 實(shí)例,再由 SplitChunksPlugin 插件根據(jù)規(guī)則與 ChunkGraph 對(duì) Chunk 做一系列的變化田度、拆解妒御、合并操作,重新組織成一批性能更高的 Chunks镇饺。后續(xù)再為它們做排序和生成hash等一系列優(yōu)化處理乎莉,直到 Compiler.compile 執(zhí)行完成作為資源輸出(emitAssets)。
bundle(包) 是 webpack 進(jìn)程執(zhí)行完畢后輸出的最終結(jié)果奸笤,是對(duì) chunk 進(jìn)行編譯壓縮打包等處理后的產(chǎn)出惋啃。通常與構(gòu)建完成的 chunk 為一對(duì)一的關(guān)系。但也有例外监右,比如:
- 當(dāng)我們給 webpack 配置了生成 SourceMap 的選項(xiàng) (
devtool: 'source-map'
):
// webpack 配置
module.exports = {
entry: {
app: './src/index.js',
},
mode: 'development',
devtool: 'source-map',
output: {
path: __dirname + '/dist',
filename: '[name].[contenthash:6].js',
chunkFilename: '[name].[contenthash:8].js',
},
// ...
}
可以看到同一個(gè) chunk 產(chǎn)生了兩個(gè) bundle边灭,app.js 和與它對(duì)應(yīng)的 app.js.map。
-
MiniCssExtractPlugin 在 seal 的資源生成階段 - chunk 獲取 Manifest 清單文件的時(shí)候抽離出 CssModule 到單獨(dú)的文件健盒,這時(shí) chunk 關(guān)聯(lián)的
css
也算一個(gè) bundle 了绒瘦。
【mini-css-extract-plugin源碼解析】
順便說(shuō)明下上面的output.filename
和output.chunkFilename
:
-
filename
是給每個(gè)輸出的 bundle 命名的(最終的 chunk)称簿,[name]
取值為 chunk 的名稱。入口 chunk 的[name]
是 entry 配置的入口對(duì)象的 key惰帽,如上面的app
(但只有當(dāng)給 entry 傳遞對(duì)象才成立憨降,否則都是默認(rèn)的main
)。runtime chunk 則是optimization.runtimeChunk 配置的名字该酗。 - 如果配置了
chunkFilename
券册,則除了包含運(yùn)行時(shí)代碼的那個(gè) bundle,其余 bundle 的命名都應(yīng)用chunkFilename
如單獨(dú)抽出 runtime chunk垂涯,那么 runtime 應(yīng)用output.filename
烁焙,其余都應(yīng)用output.chunkFilename
;否則就是包含入口模塊的 chunk 應(yīng)用output.filename
耕赘,其余用output.chunkFilename
骄蝇。
原理:看了源碼就是 chunk 資源構(gòu)建階段觸發(fā)了
template.hooks:renderManifest
,會(huì)執(zhí)行插件 JavascriptModulesPlugin 的相關(guān)方法操骡。根據(jù)模版的不同執(zhí)行的方法也不同九火,mainTemplate
負(fù)責(zé)生成包含 runtime 的 chunk 資源,應(yīng)用的文件名模版是outputOptions.filename
册招;chunkTemplate
負(fù)責(zé)其他 chunk岔激,應(yīng)用的文件名模版是outputOptions.chunkFilename
。后面 TemplatedPathPlugin 插件在監(jiān)聽到 assetPath hook 時(shí)根據(jù)這個(gè)名字模版是掰,把文件名中的占位符如[chunkhash:8]
虑鼎,替換成 chunk hash 值。這個(gè) hash 值存在當(dāng)前 chunk 的 清單文件數(shù)據(jù)(通過(guò) template.getRenderManifest 得到)中键痛,而 hash 是 chunk 創(chuàng)建后的優(yōu)化階段生成的 (我真能bb??)
總結(jié):我們開發(fā)的時(shí)候是 module炫彩,webpack 處理時(shí)是 chunk,最后生成瀏覽器可以直接運(yùn)行的 bundle絮短。Chunk是過(guò)程中的代碼塊江兢,Bundle是結(jié)果的代碼塊。
Module 主要作用在 webpack 編譯過(guò)程的前半段丁频,解決原始資源「如何讀」的問(wèn)題杉允;而 Chunk 則主要作用在編譯的后半段,解決編譯產(chǎn)物「如何寫」的問(wèn)題席里,兩者合作搭建起 webpack 搭建主流程叔磷。
chunk 的默認(rèn)分包規(guī)則有:
- 同一個(gè) entry 入口模塊與它的同步依賴(直接/間接) 組織成一個(gè) chunk,還包含 runtime (webpackBootstrap 自執(zhí)行函數(shù)的形式)胁勺。
- 每一個(gè)異步模塊與它的同步依賴單獨(dú)組成一個(gè) chunk世澜。其中只會(huì)包含入口 chunk 中不存在的同步依賴;若存在同步第三方包署穗,也會(huì)被單獨(dú)打包成一個(gè) chunk寥裂。
在
seal
階段開始后嵌洼,遍歷入口對(duì)象_preparedEntrypoints
,為每一個(gè)入口初始化生成 chunk 和 entryPoint封恰,入口 chunk 此時(shí)只有入口模塊本身麻养,與它的依賴真正建立聯(lián)系要在后面生成chunk graph
時(shí)。
在生成入口 chunk 后诺舔,會(huì)執(zhí)行buildChunkGraph
方法鳖昌,借助ModuleDependencyGraph
中存儲(chǔ)的依賴關(guān)系,生成 chunk graph (chunk 依賴圖)低飒。
chunk graph 是 webpack 輸出最終 chunk 的依據(jù)许昨,它的構(gòu)建有兩個(gè)階段,生成 basic chunk graph 和 優(yōu)化 chunk graph褥赊。
我們從
buildChunkGraph
的三個(gè)子方法按順序來(lái)詳解:
1.visitModules
:
遍歷compilation.modules
建立起基本的 Module Graph (模塊依賴圖)糕档,為遍歷異步依賴(block)等所用。先處理入口 chunk 的所有同步依賴拌喉,遍歷時(shí)優(yōu)先將同步依賴嵌套的同步模塊添加完再去處理平級(jí)的同步依賴速那。然后按每個(gè)異步依賴的父模塊被處理的順序依次生成異步 chunk 和 chunkGroup。
然后遍歷 module graph尿背,為入口模塊和它所有(直接/間接)同步依賴形成一個(gè) EntryPoint(繼承自 ChunkGroup)端仰,入口 chunk此時(shí)才會(huì)建立起與其依賴模塊的聯(lián)系。為所有異步模塊和它的同步依賴生成一個(gè) chunk 和 chunkGroup(會(huì)重復(fù))田藐。如 chunk 的同步模塊已存在于入口 chunk荔烧,則不會(huì)再存入它的_modules
中。此階段初始生成了 chunk graph(chunk 依賴圖)坞淮。
2.connectChunkGroups
:檢查入口 chunk 和 有異步依賴的異步 chunk, 如果它們的子 chunk 有它們未包含的新模塊茴晋,就建立它們各自所屬 chunkGroup 的 父子關(guān)系陪捷。
3.cleanupUnconnectedGroups
:找到?jīng)]有父 chunkgroup 的 chunkgroup回窘,刪除它里面的 chunk,并解除與相關(guān) module市袖、chunk啡直、chunkGroup 的關(guān)系。
2苍碟、3 階段對(duì) chunk graph 進(jìn)行了優(yōu)化酒觅,去除了 由已存在于入口 chunk 中的 模塊創(chuàng)建的異步 chunk。
buildChunkGraph
也可以說(shuō)是 chunk 生成階段微峰,compilation.hooks.afterChunks
觸發(fā)之后就進(jìn)入 chunk 優(yōu)化階段:
暴露很多 chunk 優(yōu)化相關(guān)鉤子:觸發(fā) optimize 相關(guān) hook 移除空 chunk 和 重復(fù) chunk舷丹,如配置了SplitChunksPlugin也會(huì)在此時(shí)再對(duì) chunk 進(jìn)行組合/分割;
然后觸發(fā)其他 hook 分別設(shè)置 module.id蜓肆、chunk.id 并對(duì)它們進(jìn)行排序颜凯。以及各類 hash 的創(chuàng)建谋币。
下面我們來(lái)具體舉例說(shuō)明。比如我們有一個(gè)入口 index症概,全部關(guān)聯(lián)文件如下:
注:@
是 src/ 目錄的別名
// a.js 入口文件
// src/a.js
import { add } from '@/b'
import('@/c').then(c => c.sub(2, 1))
const a = 1
add(3, 2 + a)
console.log(e)
// src/b.js
import mul from '@/d'
import('@/f').then(({f}) => console.log(f))
export function add(n1, n2) {
return n1 + n2 + mul(10, 5)
}
export function unusedAdd(n1, n2) {
return n1 + n2 * n2
}
// src/d.js
export default function mul(n1, n2) {
const x = 10000
return n1 * n2 + x
}
// src/c.js
import mul from '@/d'
import e from '@/e'
import('@/b').then(b => b.add(200, 100))
console.log(e)
export function sub(n1, n2) {
return n1 - n2 + mul(100, 50)
}
// src/e.js
export default 'e'
// src/f.js
import { sub } from '@/c'
sub(6,8)
export const f = 'f'
webpack 配置:
// webpack.config.js
module.exports = {
entry: {
index: "./src/a",
}
// 省略上面相同的 output 蕾额、mode 等配置
};
先看結(jié)果,a
彼城、b
诅蝶、d
打包進(jìn)入口 chunk (index.js
),c
和它同步依賴e
組成 chunk[1]募壕、f
和它同步依賴c
调炬、e
組成 chunk[0]
根據(jù)默認(rèn)分包規(guī)則為何輸出這幾個(gè) chunk 不難理解,可能唯一需要捋的是
(1)c
的同步依賴d
和 (2) 異步依賴b
webpack 是怎么處理的舱馅?(3)還有c
作為入口的異步依賴筐眷,又被異步模塊同步引入的情況。
再回去看我們上面大段 chunk 生成/優(yōu)化流程:生成初始 chunk graph 階段c
的同步依賴d
時(shí)發(fā)現(xiàn)d
已經(jīng)存在于入口 chunk习柠,故不會(huì)再存入c
所在的 chunk 中匀谣,疑問(wèn)(1)解決。b
在此時(shí)會(huì)生成一個(gè)異步 chunk 和 一個(gè) chunkGroup资溃。
此時(shí) chunk 的順序 和 圖示 chunkGroup 一致武翎。
接著對(duì) chunk graph 進(jìn)行優(yōu)化,去除了 由已存在于入口 chunk 中的b
模塊創(chuàng)建的異步 chunk溶锭。疑問(wèn)(2)解決宝恶。
不從原理角度也很好理解b
已經(jīng)存在于入口 chunk 了,項(xiàng)目運(yùn)行時(shí)入口 chunk 會(huì)先于其他異步 chunk 加載趴捅。屆時(shí)已經(jīng)能獲取到b
垫毙,沒(méi)有必要再去異步獲取。
至于c
為什么被重復(fù)打包進(jìn)f
生成的異步 chunk 從原理也好理解了拱绑,因?yàn)橥揭蕾?strong>c
不存在于入口 chunk综芥。疑問(wèn)(3)解決。
具體過(guò)程很復(fù)雜猎拨,可以看下這篇【webpack系列之六chunk graph圖生成】
另外膀藐,重復(fù)引入的異步塊,最終只會(huì)生成一個(gè)異步 chunk (本例沒(méi)有體現(xiàn))红省。在chunk graph 優(yōu)化完畢额各,chunk 優(yōu)化階段會(huì)借助訂閱 hook 的插件實(shí)現(xiàn) chunk 去重和 刪除空 chunk、給 chunk 排序等吧恃。
默認(rèn)打包規(guī)則存在的問(wèn)題
第一個(gè)缺點(diǎn):重復(fù)打包模塊
實(shí)際項(xiàng)目中公共模塊的數(shù)量和 size 和 demo 不是一個(gè)量級(jí)虾啦。像上例c
、e
這樣重復(fù)打包的問(wèn)題就會(huì)尤其顯著。 雖然 SplitChunksPlugin 插件的默認(rèn)配置會(huì)起作用傲醉,比如不同 chunk 中大于20kb
且從屬于異步 chunk的公共模塊(公用 >= 2次)會(huì)被抽離出來(lái)针饥。但這明顯不足以適用所有情況。
多入口配置也會(huì)造成這個(gè)問(wèn)題需频,比如現(xiàn)在添加了一個(gè) admin 入口:
// webpack.config.js
module.exports = {
entry: {
index: "./src/a",
admin: "./src/b"
}
};
不同入口有相同的同步依賴會(huì)重復(fù)打包丁眼,有相同的異步依賴則不會(huì)(只單獨(dú)打包一次)。
看框出部分昭殉,兩個(gè)入口共同依賴b
和b
苞七,b
、b
就被重復(fù)打包在index.js
和admin.js
里挪丢,因?yàn)椴煌肟?chunk 相互獨(dú)立蹂风。這會(huì)造成不必要的性能損耗。合理利用 SplitChunksPlugin 能夠更高效乾蓬、智能地實(shí)現(xiàn)「啟發(fā)式分包」惠啄,這里涉及的不在此展開,可移步另一篇【webpack SplitChunksPlugin 配置詳解】任内。
另外的不足:
- 每次打包都會(huì)變動(dòng)的 runtime 包含在入口 chunk撵渡,會(huì)影響入口 chunk 文件的本地緩存
- 第三方插件、UI庫(kù)這種變動(dòng)很少的模塊作為同步依賴和別的模塊打包在一個(gè) chunk 中死嗦,無(wú)法利用瀏覽器緩存
- SplitChunksPlugin 代碼分割插件默認(rèn)只處理異步 chunk
runtime 分包
出于性能考慮通常會(huì)將入口 chunk 中的 runtime 單獨(dú)抽離趋距。
配置方法:entry.runtime (webpack5) 或 optimization.runtimeChunk
同樣是多入口情況,如果不抽離也會(huì)重復(fù)生成多份 runtime 代碼(在每個(gè)入口 chunk 中)越除,如果像下面這樣抽出节腐,兩個(gè)入口就可以共用一份運(yùn)行時(shí) chunk 了。
module.exports = {
entry: {
// 不同入口為 runtime 屬性設(shè)置同樣的名稱即可共享一個(gè) runtime chunk
index: { import: "./src/index", runtime: "runtime" }, // webpack 5 支持
admin: { import: "./src/admin", runtime: "runtime" },
}
// 或者
// optimization: {
// runtimeChunk: {
// name: 'runtime',
// },
// }
};
runtime 呈現(xiàn)為一個(gè)自執(zhí)行函數(shù)摘盆,包含模塊交互時(shí)連接模塊所需的加載和解析邏輯的所有代碼翼雀。它負(fù)責(zé)項(xiàng)目的運(yùn)行,webpack 通過(guò)它來(lái)連接模塊化應(yīng)用程序孩擂。它不僅與業(yè)務(wù)代碼聯(lián)系緊密狼渊,還伴隨著 manifest 數(shù)據(jù)(chunks 映射關(guān)系的 list),每個(gè) chunk 的 id 都是基于內(nèi)容 hash 出來(lái)的肋殴,所以每次改動(dòng)都會(huì)影響它囤锉,如果打包進(jìn)入口 chunk 等于入口文件(如 index.js)的緩存每次都會(huì)失效,這顯然不是我們想要的护锤。
原理:compilation.hooks: optimizeChunksAdvanced
鉤子事件被觸發(fā)的時(shí)候RuntimeChunkPlugin 的監(jiān)聽事件會(huì)響應(yīng) (SplitChunksPlugin 插件也是這時(shí)處理的):這時(shí)候默認(rèn)規(guī)則的 chunk 已分包(組)完成 (入口 和 異步),如有配置 optimization.runtimeChunk酿傍,會(huì)在這此基礎(chǔ)上抽離出 runtime 代碼烙懦。
編譯時(shí),webpack 會(huì)根據(jù)業(yè)務(wù)代碼決定輸出哪些支撐特性的運(yùn)行時(shí)代碼(基于 Dependency 子類)赤炒,例如:
需要 webpack_require.f氯析、webpack_require.r 等方法實(shí)現(xiàn)最起碼的模塊化支持亏较;
如果有用到動(dòng)態(tài)加載特性,則需要寫入 webpack_require.e 函數(shù)掩缓;
如果用到 Module Federation 特性雪情,則需要寫入 webpack_require.o 函數(shù)
等...
在實(shí)際項(xiàng)目中,單獨(dú)的 runtimeChunk 只是用于驅(qū)動(dòng)不同頁(yè)面路由和組件的加載你辣,代碼量比較小巡通,而這個(gè)文件經(jīng)常改變,每次都需要重新請(qǐng)求它舍哄。它的 http 耗時(shí)遠(yuǎn)大于它的執(zhí)行時(shí)間了宴凉,所以通常的做法是將它抽出再內(nèi)聯(lián)到我們的 index.html 之中(index.html 本來(lái)每次打包都會(huì)變)。配置 optimization.runtimeChunk 抽離表悬,使用 script-ext-html-webpack-plugin
插件將其內(nèi)聯(lián)在 index.html 弥锄。
示例:vue-cli 項(xiàng)目在 vue.config.js 用 webpack-chain 配置:
chainWebpack(config) {
config
.plugin('ScriptExtHtmlWebpackPlugin')
.after('html')
.use('script-ext-html-webpack-plugin', [{
// `runtime` must same as runtimeChunk name. default is `runtime`
// 正則匹配 runtime 文件名
inline: /runtime\..*\.js$/
}])
.end()
config.optimization.runtimeChunk('single')
}
關(guān)于 chunk 更詳細(xì)的編譯步驟可以參考【淺析 webpack 打包流程(原理) 三 - 生成 chunk】、【淺析 webpack 打包流程(原理) 四 - chunk 優(yōu)化】
有點(diǎn)難的知識(shí)點(diǎn): Webpack Chunk 分包規(guī)則詳解
Webpack 理解 Chunk
webpack系列之六chunk圖生成