全文 2500 字,閱讀時長約 30 分鐘。若是以爲文章有用,歡迎點贊關注,但寫做實屬不易,未經做者贊成,禁止任何形式轉載!!!
在前面系列文章提到,webpack 實現中,原始的資源模塊以 Module
對象形式存在、流轉、解析處理。javascript
而 Chunk
則是輸出產物的基本組織單位,在生成階段 webpack 按規則將 entry
及其它 Module
插入 Chunk
中,以後再由 SplitChunksPlugin
插件根據優化規則與 ChunkGraph
對 Chunk
作一系列的變化、拆解、合併操做,從新組織成一批性能(可能)更高的 Chunks
。運行完畢以後 webpack 繼續將 chunk
一一寫入物理文件中,完成編譯工做。java
綜上,Module
主要做用在 webpack 編譯過程的前半段,解決原始資源「如何讀」的問題;而 Chunk 對象則主要做用在編譯的後半段,解決編譯產物「如何寫」的問題,二者合做搭建起 webpack 搭建主流程。webpack
Chunk 的編排規則很是複雜,涉及 entry、optimization 等諸多配置項,我打算分紅兩篇文章分別講解基本分包規則、SplitChunksPlugin
分包優化規則,本文將集中在第一部分,講解 entry、異步模塊、runtime 三條規則的細節與原理。web
關注公衆號【Tecvan】,回覆【1】,獲取 Webpack 知識體系腦圖
Webpack 4 以後編譯過程大體上能夠拆解爲四個階段(參考:[萬字總結] 一文吃透 Webpack 核心原理):promise
在構建(make) 階段,webpack 從 entry 出發根據模塊間的引用關係(require/import) 逐步構建出模塊依賴關係圖(ModuleDependencyGraph),依賴關係圖表達了模塊與模塊之間互相引用的前後次序,基於這種次序 webpack 就能夠推斷出模塊運行以前須要先執行那些依賴模塊,也就能夠進一步推斷出那些模塊應該打包在一塊兒,那些模塊能夠延後加載(異步執行),關於模塊依賴圖的更多信息,能夠參考我另外一篇文章 《有點難的 webpack 知識點:Dependency Graph 深度解析》。架構
到了生成(seal) 階段,webpack 會根據模塊依賴圖的內容組織分包 —— Chunk 對象,默認的分包規則有:異步
entry
下觸達到的模塊組織成一個 chunkentry.runtime
單獨組織成一個 chunk默認規則集中在 compilation.seal
函數實現,seal 核心邏輯運行結束後會生成一系列的 Chunk
、ChunkGroup
、ChunkGraph
對象,後續如 SplitChunksPlugin
插件會在 Chunk
系列對象上作進一步的拆解、優化,最終反映到輸出上纔會表現出複雜的分包結果。async
咱們聊聊默認生成規則。模塊化
重點:seal 階段遍歷 entry 對象,爲每個 entry 單獨生成 chunk,以後再根據模塊依賴圖將 entry 觸達到的全部模塊打包進 chunk 中。
在生成階段,Webpack 首先根據遍歷用戶提供的 entry 屬性值,爲每個 entry 建立 Chunk 對象,好比對於以下配置:函數
module.exports = { entry: { main: "./src/main", home: "./src/home", } };
Webpack 遍歷 entry 對象屬性並建立出 chunk[main]
、chunk[home]
兩個對象,此時兩個 chunk 分別包含 main
、home
模塊:
初始化完畢後,Webpack 會讀取 ModuleDependencyGraph
的內容,將 entry 所對應的內容塞入對應的 chunk (發生在 webpack/lib/buildChunkGrap.js
文件)。好比對於以下文件依賴:
main.js 以同步方式直接或間接引用了 a/b/c/d 四個文件,分析 ModuleDependencyGraph
過程會逐步將 a/b/c/d 模塊逐步添加到 chunk[main]
中,最終造成:
PS: 基於動態加載生成的 chunk 在 webpack 官方文檔中,一般稱之爲 Initial chunk 。
重點:分析
ModuleDependencyGraph
時,每次遇到異步模塊都會爲之建立單獨的 Chunk 對象,單獨打包異步模塊。
Webpack 4 以後,只須要用異步語句 require.ensure("./xx.js")
或 import("./xx.js")
方式引入模塊,就能夠實現模塊的動態加載,這種能力本質也是基於 Chunk 實現的。
Webpack 生成階段中,遇到異步引入語句時會爲該模塊單獨生成一個 chunk
對象,並將其子模塊都加入這個 chunk 中。例如對於下面的例子:
// index.js, entry 文件 import 'sync-a' import 'sync-b' import('async-c')
在 index.js
中,以同步方式引入 sync-a
、sync-b
;以異步方式引入 async-a
模塊;同時,在 · 中以同步方式引入 · 模塊。對應的模塊依賴如:
此時,webpack 會爲入口 index.js
、異步模塊 async-a.js
分別建立分包,造成以下數據:
這裏須要引入一個新的概念 —— Chunk
間的父子關係。由 entry
生成的 Chunk
之間相互孤立,沒有必然的先後依賴關係,但異步生成的 Chunk
則不一樣,引用者(上例 index.js
塊)須要在特定場景下使用被引用者(上例 async-a
塊),二者間存在單向依賴關係,在 webpack 中稱引用者爲 parent、被引用者爲 child,分別存放在 ChunkGroup._parents
、ChunkGroup._children
屬性中。
上述分包方案默認狀況下會生成兩個文件:
index
對應的 index.js
async-a
對應的 src_async-a_js.js
運行時,webpack 在 index.js 中使用 promise 及 __webpack_require__.e
方法異步載入並運行文件 src_async-a_js.js
,從而實現動態加載。
PS: 基於異步模塊的 chunk 在 webpack 官方文檔中,一般稱之爲 Async chunk 。
重點: Webpack 5 以後還能根據
entry.runtime
配置單獨打包運行時代碼。
除了 entry、異步模塊外,webpack 5以後還支持基於 runtime
的分包規則。除業務代碼外,Webpack 編譯產物中還須要包含一些用於支持 webpack 模塊化、異步加載等特性的支撐性代碼,這類代碼在 webpack 中被統稱爲 runtime
。舉個例子,產物中一般會包含以下代碼:
/******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = {}; // The module cache /************************************************************************/ /******/ /******/ var __webpack_module_cache__ = {}; // The require function /******/ /******/ /******/ function __webpack_require__(moduleId) { /******/ /******/ __webpack_modules__[moduleId]( module, module.exports, __webpack_require__ ); // Return the exports of the module /******/ /******/ /******/ return module.exports; /******/ } // expose the modules object (__webpack_modules__) /******/ /******/ /******/ __webpack_require__.m = __webpack_modules__; /* webpack/runtime/compat get default export */ /******/ // ... })();
編譯時,Webpack 會根據業務代碼決定輸出那些支撐特性的運行時代碼(基於 Dependency
子類),例如:
__webpack_require__.f
、__webpack_require__.r
等功能實現最起碼的模塊化支持__webpack_require__.e
函數__webpack_require__.o
函數雖然每段運行時代碼可能都很小,但隨着特性的增長,最終結果會愈來愈大,特別對於多 entry 應用,在每一個入口都重複打包一份類似的運行時代碼顯得有點浪費,爲此 webpack 5 專門提供了 entry.runtime
配置項用於聲明如何打包運行時代碼。用法上只需在 entry 項中增長字符串形式的 runtime
值,例如:
module.exports = { entry: { index: { import: "./src/index", runtime: "solid-runtime" }, } };
Webpack 執行完 entry
、異步模塊分包後,開始遍歷 entry
配置判斷是否帶有 runtime
屬性,若是有則建立以 runtime
值爲名的 Chunk
,所以,上例配置將生成兩個chunk:chunk[index.js]
、chunk[solid-runtime]
,並據此最終產出兩個文件:
index.js
文件solid-runtime.js
文件在多 entry 場景中,只要爲每一個 entry 都設定相同的 runtime 值,webpack 運行時代碼最終就會集中寫入到同一個 chunk,例如對於以下配置:
module.exports = { entry: { index: { import: "./src/index", runtime: "solid-runtime" }, home: { import: "./src/home", runtime: "solid-runtime" }, } };
入口 index、home 共享相同的 runtime
,最終生成三個 chunk
,分別爲:
同時生成三個文件:
index.js
home.js
solid-runtime.js
至此,webpack 分包規則的基本邏輯就介紹完畢了,實現上,大部分功能代碼都集中在:
webpack/lib/compilation.js
文件的 seal
函數webpack/lib/buildChunkGraph.js
的 buildChunkGraph
函數默認分包規則最大的問題是沒法解決模塊重複,若是多個 chunk 同時包含同一個 module
,那麼這個 module
會被不受限制地重複打包進這些 chunk。好比假設咱們有兩個入口 main/index
同時依賴了同一個模塊:
默認狀況下,webpack 不會對此作額外處理,只是單純地將 c 模塊同時打包進 main/index
兩個 chunk,最終造成:
能夠看到 chunk 間互相孤立,模塊 c 被重複打包,對最終產物可能形成沒必要要的性能損耗!
爲了解決這個問題,webpack 3 引入 CommonChunkPlugin
插件試圖將 entry 之間的公共依賴提取成單獨的 chunk,但 CommonChunkPlugin
本質上是基於 Chunk 之間簡單的父子關係鏈實現的,很難推斷出提取出的第三個包應該做爲 entry 的父 chunk 仍是子 chunk,CommonChunkPlugin
統一處理爲父 chunk,某些狀況下反而對性能形成了不小的負面影響。
在 webpack 4 以後則引入了更負責的設計 —— ChunkGroup
專門實現關係鏈管理,配合 SplitChunksPlugin
可以更高效、智能地實現啓發式分包,這裏的內容很複雜,我打算拆開來在下一篇文章再講,感興趣的同窗記得關注。
後面我還會繼續 focus 在 chunk 相關功能與核心實現原理,內容包括:
ChunkGroup
的引入解決了什麼問題,爲何能極大優化分包功能ChunkGraph
解決了什麼問題感興趣的同窗必定要記得點贊關注,您的反饋將是我持續創做的巨大動力!
往期文章: