全文 5000 字,深度剖析 Webpack 運行時的內容、結構與生成原理,歡迎點贊關注。寫做不易,未經做者贊成,禁止任何形式轉載!!!
在上一篇文章 有點難的 webpack 知識點:Chunk 分包規則詳解 中,咱們詳細講解了 Webpack 默認的分包規則,以及一部分 seal 階段的執行邏輯,如今咱們將按 Webpack 的執行流程,繼續往下深度分析實現原理,具體內容包括:javascript
bundle
?實際上,本文及前面幾篇原理性質的文章,可能並不能立刻解決你在業務中可能正在面臨的現實問題,但放到更長的時間維度,這些文章所呈現的知識、思惟、思辨過程可能可以長遠地給到你:html
因此,但願感興趣的同窗可以堅持,我後續還會輸出不少關於 Webpack 實現原理的文章!若是你剛好也想提高本身在 Webpack 方面的知識儲備,關注我,咱們一塊兒學習!java
爲了正常、正確運行業務項目,Webpack 須要將開發者編寫的業務代碼以及支撐、調配這些業務代碼的運行時一併打包到產物(bundle)中,以建築做類比的話,業務代碼至關於磚瓦水泥,是看得見摸得着能直接感知的邏輯;運行時至關於掩埋在磚瓦之下的鋼筋地基,一般不會關注但決定了整座建築的功能、質量。webpack
大多數 Webpack 特性都須要特定鋼筋地基才能跑起來,好比說:web
下面先從最簡單的示例開始,逐步展開了解各個特性下的 Webpack 運行時代碼。segmentfault
先從一個最簡單的示例開始,對於下面的代碼結構:數組
// a.js export default 'a module'; // index.js import name from './a' console.log(name)
使用以下配置:promise
module.exports = { entry: "./src/index", mode: "development", devtool: false, output: { filename: "[name].js", path: path.join(__dirname, "./dist"), }, };
配置的內容比較簡單,就不展開講了,直接看編譯生成的結果:瀏覽器
雖然看起來很非主流,但細心分析仍是能拆解出代碼脈絡的,bundle 總體由一個 IIFE 包裹,裏面的內容從上到下依次爲:緩存
__webpack_modules__
對象,包含了除入口外的全部模塊,示例中即 a.js
模塊__webpack_module_cache__
對象,用於存儲被引用過的模塊__webpack_require__
函數,實現模塊引用(require) 邏輯__webpack_require__.d
,工具函數,實現將模塊導出的內容附加的模塊對象上__webpack_require__.o
,工具函數,判斷對象屬性用__webpack_require__.r
,工具函數,在 ESM 模式下聲明 ESM 模塊標識index.js
,用於啓動整個應用這幾個 __webpack_
開頭奇奇怪怪的函數能夠統稱爲 Webpack 運行時代碼,做用如前面所說的是搭起整個業務項目的骨架,就上述簡單示例所羅列出來的幾個函數、對象而言,它們協做構建起一個簡單的模塊化體系從而實現 ES Module 規範所聲明的模塊化特性。
上述示例中最終的函數是 __webpack_require__
,它實現了模塊間引用功能,核心代碼:
function __webpack_require__(moduleId) { /******/ // 若是模塊被引用過 /******/ var cachedModule = __webpack_module_cache__[moduleId]; /******/ if (cachedModule !== undefined) { /******/ return cachedModule.exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = (__webpack_module_cache__[moduleId] = { /******/ // no module.id needed /******/ // no module.loaded needed /******/ exports: {}, /******/ }); /******/ /******/ // Execute the module function /******/ __webpack_modules__[moduleId]( module, module.exports, __webpack_require__ ); /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ }
從代碼能夠推測出,它的功能:
moduleId
參數找到對應的模塊代碼,執行並返回結果moduleId
對應的模塊被引用過,則直接返回存儲在 __webpack_module_cache__
緩存對象中的導出內容,避免重複執行其中,業務模塊代碼被存儲在 bundle 最開始的 __webpack_modules__
變量中,內容如:
var __webpack_modules__ = { "./src/a.js": ( __unused_webpack_module, __webpack_exports__, __webpack_require__ ) => { // ... }, };
結合 __webpack_require__
函數與 __webpack_modules__
變量就能夠正確地引用到代碼模塊,例如上例生成代碼最後面的IIFE:
(() => { /*!**********************!*\ !*** ./src/index.js ***! \**********************/ /* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ "./src/a.js"); console.log(_a__WEBPACK_IMPORTED_MODULE_0__.name); })();
這幾個函數、對象構成了 Webpack 運行時最基本的能力 —— 模塊化,它們的生成規則與原理咱們放到文章第二節《實現原理》再講,下面咱們繼續看看異步模塊加載、模塊熱更新場景下對應的運行時內容。
咱們來看個簡單的異步模塊加載示例:
// ./src/a.js export default "module-a" // ./src/index.js import('./a').then(console.log)
Webpack 配置跟上例類似:
module.exports = { entry: "./src/index", mode: "development", devtool: false, output: { filename: "[name].js", path: path.join(__dirname, "./dist"), }, };
生成的代碼太長,就不貼了,相比於最開始的基本結構示例所示的模塊化功能,使用異步模塊加載特性時,會額外增長以下運行時:
__webpack_require__.e
:邏輯上包裹了一層中間件模式與 promise.all
,用於異步加載多個模塊__webpack_require__.f
:供 __webpack_require__.e
使用的中間件對象,例如使用 Module Federation 特性時就須要在這裏註冊中間件以修改 e 函數的執行邏輯__webpack_require__.u
:用於拼接異步模塊名稱的函數__webpack_require__.l
:基於 JSONP 實現的異步模塊加載函數__webpack_require__.p
:當前文件的完整 URL,可用於計算異步模塊的實際 URL建議讀者運行示例對比實際生成代碼,感覺它們的具體功能。這幾個運行時模塊構建起 Webpack 異步加載能力,其中最核心的是 __webpack_require__.e
函數,它的代碼很簡單:
__webpack_require__.f = {}; /******/ // This file contains only the entry chunk. /******/ // The chunk loading function for additional chunks /******/ __webpack_require__.e = (chunkId) => { /******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => { /******/ __webpack_require__.f[key](chunkId, promises); /******/ return promises; /******/ }, [])); /******/ };
從代碼看,只是實現了一套基於 __webpack_require__.f
的中間件模式,以及用 Promise.all
實現並行處理,實際加載工做由 __webpack_require__.f.j
與 __webpack_require__.l
實現,分開來看兩個函數:
/******/ __webpack_require__.f.j = (chunkId, promises) => { /******/ // JSONP chunk loading for javascript /******/ var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined; /******/ if(installedChunkData !== 0) { // 0 means "already installed". /******/ /******/ // a Promise means "currently loading". /******/ if(installedChunkData) { /******/ promises.push(installedChunkData[2]); /******/ } else { /******/ if(true) { // all chunks have JS /******/ // ... /******/ // start chunk loading /******/ var url = __webpack_require__.p + __webpack_require__.u(chunkId); /******/ // create error before stack unwound to get useful stacktrace later /******/ var error = new Error(); /******/ var loadingEnded = ...; /******/ __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId); /******/ } else installedChunks[chunkId] = 0; /******/ } /******/ } /******/ };
__webpack_require__.f.j
實現了異步 chunk
路徑的拼接、緩存、異常處理三個方面的邏輯,而 __webpack_require__.l
函數:
/******/ var inProgress = {}; /******/ // data-webpack is not used as build has no uniqueName /******/ // loadScript function to load a script via script tag /******/ __webpack_require__.l = (url, done, key, chunkId) => { /******/ if(inProgress[url]) { inProgress[url].push(done); return; } /******/ var script, needAttach; /******/ if(key !== undefined) { /******/ var scripts = document.getElementsByTagName("script"); /******/ // ... /******/ } /******/ // ... /******/ inProgress[url] = [done]; /******/ var onScriptComplete = (prev, event) => { /******/ // ... /******/ } /******/ ; /******/ var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000); /******/ script.onerror = onScriptComplete.bind(null, script.onerror); /******/ script.onload = onScriptComplete.bind(null, script.onload); /******/ needAttach && document.head.appendChild(script); /******/ };
__webpack_require__.l
中經過 script 實現異步 chunk 內容的加載與執行。
e + l + f.j
三個運行時函數支撐起 Webpack 異步模塊運行的能力,落到實際用法上只須要調用 e 函數便可完成異步模塊加載、運行,例如上例對應生成的 entry
內容:
/*!**********************!*\ !*** ./src/index.js ***! \**********************/ __webpack_require__.e(/*! import() */ "src_a_js").then(__webpack_require__.bind(__webpack_require__, /*! ./a */ "./src/a.js"))
模塊熱更新 —— HMR 是一個能顯著提升開發效率的能力,它可以在模塊代碼出現變化的時候,單獨編譯該模塊並將最新的編譯結果傳送到瀏覽器,瀏覽器再用新的模塊代碼替換掉舊的代碼,從而實現模塊級別的代碼熱替換能力。落到最終體驗上,開發者啓動 Webpack 後,編寫、修改代碼的過程當中不須要手動刷新瀏覽器頁面,全部變動可以實時同步呈現到頁面中。
實現上,HMR 的實現鏈路很長也比較有意思,咱們後續會單開一篇文章討論,本文主要關注 HMR 特性所帶入運行時代碼。啓動 HMR 能力須要用到一些特殊的配置項:
module.exports = { entry: "./src/index", mode: "development", devtool: false, output: { filename: "[name].js", path: path.join(__dirname, "./dist"), }, // 簡單起見,這裏使用 HtmlWebpackPlugin 插件自動生成做爲 host 的 html 文件 plugins: [ new HtmlWebpackPlugin({ title: "Hot Module Replacement", }), ], // 配置 devServer 屬性,啓動 HMR devServer: { contentBase: "./dist", hot: true, writeToDisk: true, },
按照上述配置,使用命令 webpack serve --hot-only
啓動 Webpack,就能夠在 dist 文件夾找到產物:
相比於前面兩個示例,HMR 所產生運行時代碼達到 1.5w+ 行,簡直能夠用炸裂來形容。主要的運行時內容有:
webpack-dev-server
、webpack/hot/xxx
、querystring
等框架,這一部分佔了大部分代碼__webpack_require__.l
:與異步模塊加載同樣,基於 JSONP 實現的異步模塊加載函數__webpack_require__.e
:與異步模塊加載同樣__webpack_require__.f
:與異步模塊加載同樣__webpack_require__.hmrF
: 用於拼接熱更新模塊 url 的函數webpack/runtime/hot
:這不是單個對象或函數,而是包含了一堆實現模塊替換的方法能夠看到, HMR 運行時是上面異步模塊加載運行時的超集,而異步模塊加載的運行時又是第一個基本示例運行時的超集,層層疊加。在 HMR 中包含了:
內容過多,咱們放到下次專門開一篇文章聊聊 HMR。
仔細閱讀上述三個示例,相信讀者應該已經模模糊糊捕捉到一些重要規則:
__webpack_require__.e
函數,那麼這裏面必然有一個運行時依賴收集的過程落到 Webpack 源碼實現上,運行時的生成邏輯能夠劃分爲兩個步驟:
兩個步驟都發生在打包階段,即 Webpack(v5) 源碼的 compilation.seal
函數中:
上圖是我總結的 Webpack 知識圖譜的一部分,可關注公衆號【Tecvan】 回覆【1】獲取線上地址
注意上圖,進入 runtime 處理環節時 Webpack 已經解析得出 ModuleDependencyGraph
及 ChunkGraph
關係,也就意味着此時已經能夠計算出:
chunk
chunk
包含那些 module
,以及每一個 module
的內容chunk
與 chunk
之間的父子依賴關係對 bundle、module、chunk 關係這幾個概念還不太清晰的同窗,建議擴展閱讀:
基於這些信息,接下來首先須要收集運行時依賴。
Webpack runtime 的依賴概念上很像 Vue 的依賴,都是用來表達模塊對其它模塊存在依附關係,只是實現方法上 Vue 基於動態、在運行過程當中收集,而 Webpack 則基於靜態代碼分析的方式收集依賴。實現邏輯大體爲:
運行時依賴的計算邏輯集中在 compilation.processRuntimeRequirements
函數,代碼上包含三次循環:
module
,收集全部 module
的 runtime 依賴chunk
,將 chunk
下全部 module
的 runtime 統一收錄到 chunk
中chunk
下全部 runtime 依賴,以後遍歷全部依賴併發布 runtimeRequirementInTree
鉤子,(主要是) RuntimePlugin
插件訂閱該鉤子並根據依賴類型建立對應的 RuntimeModule
子類實例下面咱們展開聊聊細節。
在打包(seal)階段,完成 ChunkGraph
的構建以後,Webpack 會緊接着調用 codeGeneration
函數遍歷 module
數組,調用它們的 module.codeGeneration
函數執行模塊轉譯,模塊轉譯結果如:
其中,sources 屬性爲模塊通過轉譯後的結果;而 runtimeRequirements
則是基於 AST 計算出來的,爲運行該模塊時所須要用到的運行時,計算過程與本文主題無關,挖個坑下一回咱們再繼續講。
全部模塊轉譯完畢後,開始調用 compilation.processRuntimeRequirements
進入第一重循環,將上述轉譯結果的 runtimeRequirements
記錄到 ChunkGraph
對象中。
第一次循環針對 module
收集依賴,第二次循環則遍歷 chunk
數組,收集將其對應全部 module
的 runtime 依賴,例如:
示例圖中,module a
包含兩個運行時依賴;module b
包含一個運行時依賴,則通過第二次循環整合後,對應的 chunk
會包含兩個模塊對應的三個運行時依賴。
源碼中,第三次循環的代碼最少但邏輯最複雜,大體上執行三個操做:
chunk
的 runtime 依賴runtimeRequirementInTree
鉤子RuntimePlugin
監聽鉤子,並根據 runtime 依賴的標識信息建立對應的 RuntimeModule
子類對象,並將對象加入到 ModuleDepedencyGraph
和 ChunkGraph
體系中管理至此,runtime 依賴完成了從 module
內容解析,到收集,到建立依賴對應的 Module
子類,再將 Module
加入到 ModuleDepedencyGraph
/ChunkGraph
體系的全流程,業務代碼及運行時代碼對應的模塊依賴關係圖徹底 ready,能夠準備進入下一階段 —— 生成最終產物。
但在繼續講解產物邏輯以前,咱們有必要先解決兩個問題:
chunk
是什麼關係RuntimeModule
?與普通 Module
有什麼區別在上一篇文章 有點難的 webpack 知識點:Chunk 分包規則詳解 我嘗試完整地講解 Webpack 默認分包規則,回顧一下在三種特定的狀況下,Webpack 會建立新的 chunk
:
chunk
對象,稱之爲 initial chunk
chunk
對象,稱之爲 async chunk
默認狀況下 initial chunk
一般包含運行該 entry 所須要的全部 runtime 代碼,但 webpack 5 以後出現的第三條規則打破了這一限制,容許開發者將 runtime 從 initial chunk
中剝離出來獨立爲一個多 entry 間可共享的 runtime chunk
。
相似的,異步模塊對應 runtime 代碼大部分都被包含在對應的引用者身上,好比說:
// a.js export default 'a-module' // index.js // 異步引入 a 模塊 import('./a').then(console.log)
在這個示例中,index 異步引入 a 模塊,那麼按默認分配規則會產生兩個 chunk
:入口文件 index 對應的 initial chunk
、異步模塊 a 對應的 async chunk
。此時從 ChunkGraph
的角度看 chunk[index]
爲 chunk[a]
的父級,運行時代碼會被打入 chunk[index]
,站在瀏覽器的角度,運行 chunk[a]
以前必須先運行 chunk[index]
,二者造成明顯的父子關係。
在最開始閱讀 Webpack 源碼的時候,我就以爲很奇怪,Module
是 Webpack 資源管理的基本單位,但 Module
底下總共衍生出了 54 個子類,且大部分爲 Module => RuntimeModule => xxxRuntimeModule
的繼承關係:
在 有點難的 webpack 知識點:Dependency Graph 深度解析 一文中咱們聊到模塊依賴關係圖的生成過程及做用,但篇文章的內容是圍繞業務代碼展開的,用到的大可能是 NormalModule
。到 seal
函數收集運行時的過程當中,RuntimePlugin
還會爲運行時依賴一一建立對應的 RuntimeModule
子類,例如:
__webpack_require__.r
,則對應建立 MakeNamespaceObjectRuntimeModule
對象__webpack_require__.o
,則對應建立 HasOwnPropertyRuntimeModule
對象__webpack_require__.e
,則對應建立 EnsureChunkRuntimeModule
對象因此能夠推導出全部 RuntimeModule
結尾的類型與特定的運行時功能一一對應,收集依賴的結果就是在業務代碼以外建立出一堆支撐性質的 RuntimeModule
子類,這些子類對象隨後被加入 ModuleDependencyGraph
,併入整個模塊依賴體系中。
通過上面的運行時依賴收集過程後,bundle 所須要的全部內容都就緒了,接着就能夠準備寫出到文件中,即下圖核心流程中的生成(emit)階段:
個人另外一篇 [萬字總結] 一文吃透 Webpack 核心原理 對這一塊有比較細緻的講解,這裏從運行時的視角再簡單聊一下代碼流程:
compilation.createChunkAssets
,遍歷 chunks
將 chunk 對應的全部 module
,包括業務模塊、運行時模塊所有合併成一個資源(Source
子類)對象compilation.emitAsset
將資源對象掛載到 compilation.assets
屬性中compiler.emitAssets
將 assets 所有寫到 FileSystemcompiler.hooks.done
鉤子Webpack 真的很複雜,每次信心滿滿寫出一個主題的內容以後都會發現更多新的坑點,好比本文能夠衍生出來的關注點:
慢慢挖坑,慢慢填坑吧。若是以爲文章有用,請務必點贊關注轉發來一波。
往期文章