若是以爲文章有用,歡迎點贊關注,但寫做實屬不易,未經做者贊成,禁止任何形式轉載!!!
Webpack 特別難學!!!javascript
時至 5.0 版本以後,Webpack 功能集變得很是龐大,包括:模塊打包、代碼分割、按需加載、HMR、Tree-shaking、文件監聽、sourcemap、Module Federation、devServer、DLL、多進程等等,爲了實現這些功能,webpack 的代碼量已經到了驚人的程度:css
在這個數量級下,源碼的閱讀、分析、學習成本很是高,加上 webpack 官網語焉不詳的文檔,致使 webpack 的學習、上手成本極其高。爲此,社區圍繞着 Webpack 衍生出了各類手腳架,好比 vue-cli、create-react-app,解決「用」的問題。html
但這又致使一個新的問題,大部分人在工程化方面逐漸變成一個配置工程師,停留在「會用會配」可是不知道黑盒裏面究竟是怎麼轉的階段,遇到具體問題就瞎了:vue
究其緣由仍是對 webpack 內部運行機制沒有造成必要的總體認知,沒法迅速定位問題 —— 對,連問題的本質都經常看不出,所謂的不能透過現象看本質,那本質是啥?我我的將 webpack 整個龐大的體系抽象爲三方面的知識:java
三者協做構成 webpack 的主體框架:node
理解了這三塊內容就算是入了個門,對 Webpack 有了一個最最基礎的認知了,工做中再遇到問題也就能按圖索驥了。補充一句,做爲一份入門教程,本文不會展開太多 webpack 代碼層面的細節 —— 個人精力也不容許,因此讀者也不須要看到一堆文字就產生特別大的心理負擔。react
首先,咱們要理解一個點,Webpack 最核心的功能:webpack
At its core, webpack is a static module bundler for modern JavaScript applications.
也就是將各類類型的資源,包括圖片、css、js等,轉譯、組合、拼接、生成 JS 格式的 bundler 文件。官網首頁的動畫很形象地表達了這一點:git
這個過程核心完成了 內容轉換 + 資源合併 兩種功能,實現上包含三個階段:github
初始化階段:
Compiler
對象compiler
對象的 run
方法entry
找出全部的入口文件,調用 compilition.addEntry
將入口文件轉換爲 dependence
對象構建階段:
entry
對應的 dependence
建立 module
對象,調用 loader
將模塊轉譯爲標準 JS 內容,調用 JS 解釋器將內容轉換爲 AST 對象,從中找出該模塊依賴的模塊,再 遞歸 本步驟直到全部入口依賴的文件都通過了本步驟的處理生成階段:
Chunk
,再把每一個 Chunk
轉換成一個單獨的文件加入到輸出列表,這步是能夠修改輸出內容的最後機會單次構建過程自上而下按順序執行,下面會展開聊聊細節,在此以前,對上述說起的各種技術名詞不太熟悉的同窗,能夠先看看簡介:
Entry
:編譯入口,webpack 編譯的起點Compiler
:編譯管理器,webpack 啓動後會建立 compiler
對象,該對象一直存活知道結束退出Compilation
:單次編輯過程的管理器,好比 watch = true
時,運行過程當中只有一個 compiler
但每次文件變動觸發從新編譯時,都會建立一個新的 compilation
對象Dependence
:依賴對象,webpack 基於該類型記錄模塊間依賴關係Module
:webpack 內部全部資源都會以「module」對象形式存在,全部關於資源的操做、轉譯、合併都是以 「module」 爲基本單位進行的Chunk
:編譯完成準備輸出時,webpack 會將 module
按特定的規則組織成一個一個的 chunk
,這些 chunk
某種程度上跟最終輸出一一對應Loader
:資源內容轉換器,其實就是實現從內容 A 轉換 B 的轉換器Plugin
:webpack構建過程當中,會在特定的時機廣播對應的事件,插件監聽這些事件,在特定時間點介入編譯過程webpack 編譯過程都是圍繞着這些關鍵對象展開的,更詳細完整的信息,能夠參考 Webpack 知識圖譜 。
學習一個項目的源碼一般都是從入口開始看起,按圖索驥慢慢摸索出套路的,因此先來看看 webpack 的初始化過程:
解釋一下:
process.args + webpack.config.js
合併成用戶配置validateSchema
校驗配置getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults
合併出最終配置compiler
對象plugins
集合,執行插件的 apply
方法new WebpackOptionsApply().process
方法,加載各類內置插件主要邏輯集中在 WebpackOptionsApply
類,webpack 內置了數百個插件,這些插件並不須要咱們手動配置,WebpackOptionsApply
會在初始化階段根據配置內容動態注入對應的插件,包括:
EntryOptionPlugin
插件,處理 entry
配置devtool
值判斷後續用那個插件處理 sourcemap
,可選值:EvalSourceMapDevToolPlugin
、SourceMapDevToolPlugin
、EvalDevToolModulePlugin
RuntimePlugin
,用於根據代碼內容動態注入 webpack 運行時到這裏,compiler
實例就被建立出來了,相應的環境參數也預設好了,緊接着開始調用 compiler.compile
函數:
// 取自 webpack/lib/compiler.js compile(callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { // ... const compilation = this.newCompilation(params); this.hooks.make.callAsync(compilation, err => { // ... this.hooks.finishMake.callAsync(compilation, err => { // ... process.nextTick(() => { compilation.finish(err => { compilation.seal(err => {...}); }); }); }); }); }); }
Webpack 架構很靈活,但代價是犧牲了源碼的直觀性,好比說上面說的初始化流程,從建立 compiler
實例到調用 make
鉤子,邏輯鏈路很長:
lib/webpack.js
文件中 createCompiler
方法createCompiler
方法內部調用 WebpackOptionsApply
插件WebpackOptionsApply
定義在 lib/WebpackOptionsApply.js
文件,內部根據 entry
配置決定注入 entry
相關的插件,包括:DllEntryPlugin
、DynamicEntryPlugin
、EntryPlugin
、PrefetchPlugin
、ProgressPlugin
、ContainerPlugin
Entry
相關插件,如 lib/EntryPlugin.js
的 EntryPlugin
監聽 compiler.make
鉤子lib/compiler.js
的 compile
函數內調用 this.hooks.make.callAsync
EntryPlugin
的 make
回調,在回調中執行 compilation.addEntry
函數compilation.addEntry
函數內部通過一坨與主流程無關的 hook
以後,再調用 handleModuleCreate
函數,正式開始構建內容這個過程須要在 webpack 初始化的時候預埋下各類插件,經歷 4 個文件,7次跳轉纔開始進入主題,前戲太足了,若是讀者對 webpack 的概念、架構、組件沒有足夠了解時,源碼閱讀過程會很痛苦。
關於這個問題,我在文章最後總結了一些技巧和建議,有興趣的能夠滑到附錄閱讀模塊。
你有沒有思考過這樣的問題:
這些問題,基本上在構建階段都能看出一些端倪。構建階段從 entry
開始遞歸解析資源與資源的依賴,在 compilation
對象內逐步構建出 module
集合以及 module
之間的依賴關係,核心流程:
解釋一下,構建階段從入口文件開始:
handleModuleCreate
,根據文件類型構建 module
子類runLoaders
轉譯 module
內容,一般是從各種資源類型轉譯爲 JavaScript 文本遍歷 AST,觸發各類鉤子
HarmonyExportDependencyParserPlugin
插件監聽 exportImportSpecifier
鉤子,解讀 JS 文本對應的資源依賴module
對象的 addDependency
將依賴對象加入到 module
依賴列表中module.handleParseResult
處理模塊依賴module
新增的依賴,調用 handleModuleCreate
,控制流回到第一步這個過程當中數據流 module => ast => dependences => module
,先轉 AST 再從 AST 找依賴。這就要求 loaders
處理完的最後結果必須是能夠被 acorn 處理的標準 JavaScript 語法,好比說對於圖片,須要從圖像二進制轉換成相似於 export default "data:image/png;base64,xxx"
這類 base64 格式或者 export default "http://xxx"
這類 url 格式。
compilation
按這個流程遞歸處理,逐步解析出每一個模塊的內容以及 module
依賴關係,後續就能夠根據這些內容打包輸出。
假若有以下圖所示的文件依賴樹:
其中 index.js
爲 entry
文件,依賴於 a/b 文件;a 依賴於 c/d 文件。初始化編譯環境以後,EntryPlugin
根據 entry
配置找到 index.js
文件,調用 compilation.addEntry
函數觸發構建流程,構建完畢後內部會生成這樣的數據結構:
此時獲得 module[index.js]
的內容以及對應的依賴對象 dependence[a.js]
、dependence[b.js]
。OK,這就獲得下一步的線索:a.js、b.js,根據上面流程圖的邏輯繼續調用 module[index.js]
的 handleParseResult
函數,繼續處理 a.js、b.js 文件,遞歸上述流程,進一步獲得 a、b 模塊:
從 a.js 模塊中又解析到 c.js/d.js 依賴,因而再再繼續調用 module[a.js]
的 handleParseResult
,再再遞歸上述流程:
到這裏解析完全部模塊後,發現沒有更多新的依賴,就能夠繼續推動,進入下一步。
回顧章節開始時提到的問題:
Webpack 編譯過程會將源碼解析爲 AST 嗎?webpack 與 babel 分別實現了什麼?
Webpack 編譯過程當中,如何識別資源對其餘資源的依賴?
require/ import
之類的導入語句,肯定模塊對其餘資源的依賴關係相對於 grant、gulp 等流式構建工具,爲何 webpack 會被認爲是新一代的構建工具?
構建階段圍繞 module
展開,生成階段則圍繞 chunks
展開。通過構建階段以後,webpack 獲得足夠的模塊內容與模塊關係信息,接下來開始生成最終資源了。代碼層面,就是開始執行 compilation.seal
函數:
// 取自 webpack/lib/compiler.js compile(callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { // ... const compilation = this.newCompilation(params); this.hooks.make.callAsync(compilation, err => { // ... this.hooks.finishMake.callAsync(compilation, err => { // ... process.nextTick(() => { compilation.finish(err => { **compilation.seal**(err => {...}); }); }); }); }); }); }
seal
原意密封、上鎖,我我的理解在 webpack 語境下接近於 「將模塊裝進蜜罐」 。seal
函數主要完成從 module
到 chunks
的轉化,核心流程:
簡單梳理一下:
ChunkGraph
對象;compilation.modules
集合,將 module
按 entry/動態引入
的規則分配給不一樣的 Chunk
對象;compilation.modules
集合遍歷完畢後,獲得完整的 chunks
集合對象,調用 createXxxAssets
方法createXxxAssets
遍歷 module/chunk
,調用 compilation.emitAssets
方法將資 assets
信息記錄到 compilation.assets
對象中seal
回調,控制流回到 compiler
對象這一步的關鍵邏輯是將 module
按規則組織成 chunks
,webpack 內置的 chunk
封裝規則比較簡單:
entry
及 entry 觸達到的模塊,組合成一個 chunk
chunk
chunk
是輸出的基本單位,默認狀況下這些 chunks
與最終輸出的資源一一對應,那按上面的規則大體上能夠推導出一個 entry
會對應打包出一個資源,而經過動態引入語句引入的模塊,也對應會打包出相應的資源,咱們來看個示例。
假若有這樣的配置:
const path = require("path"); module.exports = { mode: "development", context: path.join(__dirname), entry: { a: "./src/index-a.js", b: "./src/index-b.js", }, output: { filename: "[name].js", path: path.join(__dirname, "./dist"), }, devtool: false, target: "web", plugins: [], };
實例配置中有兩個入口,對應的文件結構:
index-a
依賴於c,且動態引入了 e;index-b
依賴於 c/d 。根據上面說的規則:
entry
及entry觸達到的模塊,組合成一個 chunk生成的 chunks
結構爲:
也就是根據依賴關係,chunk[a]
包含了 index-a/c
兩個模塊;chunk[b]
包含了 c/index-b/d
三個模塊;chunk[e-hash]
爲動態引入 e
對應的 chunk。
不知道你們注意到沒有,chunk[a]
與 chunk[b]
同時包含了 c,這個問題放到具體業務場景可能就是,一個多頁面應用,全部頁面都依賴於相同的基礎庫,那麼這些全部頁面對應的 entry
都會包含有基礎庫代碼,這豈不浪費?爲了解決這個問題,webpack 提供了一些插件如 CommonsChunkPlugin
、SplitChunksPlugin
,在基本規則以外進一步優化 chunks
結構。
SplitChunksPlugin
的做用SplitChunksPlugin
是 webpack 架構高擴展的一個絕好的示例,咱們上面說了 webpack 主流程裏面是按 entry / 動態引入
兩種狀況組織 chunks
的,這必然會引起一些沒必要要的重複打包,webpack 經過插件的形式解決這個問題。
回顧 compilation.seal
函數的代碼,大體上能夠梳理成這麼4個步驟:
compilation.modules
,記錄下模塊與 chunk
關係module
構建 chunk 集合上面 1-3 都是預處理 + chunks 默認規則的實現,不在咱們討論範圍,這裏重點關注第4個步驟觸發的 optimizeChunks
鉤子,這個時候已經跑完主流程的邏輯,獲得 chunks
集合,SplitChunksPlugin
正是使用這個鉤子,分析 chunks
集合的內容,按配置規則增長一些通用的 chunk :
module.exports = class SplitChunksPlugin { constructor(options = {}) { // ... } _getCacheGroup(cacheGroupSource) { // ... } apply(compiler) { // ... compiler.hooks.thisCompilation.tap("SplitChunksPlugin", (compilation) => { // ... compilation.hooks.optimizeChunks.tap( { name: "SplitChunksPlugin", stage: STAGE_ADVANCED, }, (chunks) => { // ... } ); }); } };
理解了嗎?webpack 插件架構的高擴展性,使得整個編譯的主流程是能夠固化下來的,分支邏輯和細節需求「外包」出去由第三方實現,這套規則架設起了龐大的 webpack 生態,關於插件架構的更多細節,下面 plugin
部分有詳細介紹,這裏先跳過。
通過構建階段後,compilation
會獲知資源模塊的內容與依賴關係,也就知道「輸入」是什麼;而通過 seal
階段處理後, compilation
則獲知資源輸出的圖譜,也就是知道怎麼「輸出」:哪些模塊跟那些模塊「綁定」在一塊兒輸出到哪裏。seal
後大體的數據結構:
compilation = { // ... modules: [ /* ... */ ], chunks: [ { id: "entry name", files: ["output file name"], hash: "xxx", runtime: "xxx", entryPoint: {xxx} // ... }, // ... ], };
seal
結束以後,緊接着調用 compiler.emitAssets
函數,函數內部調用 compiler.outputFileSystem.writeFile
方法將 assets
集合寫入文件系統,實現邏輯比較曲折,可是與主流程沒有太多關係,因此這裏就不展開講了。
OK,上面已經把邏輯層面的構造主流程梳理完了,這裏結合資源形態流轉的角度從新考察整個過程,加深理解:
compiler.make
階段:
entry
文件以 dependence
對象形式加入 compilation
的依賴列表,dependence
對象記錄有 entry
的類型、路徑等信息dependence
調用對應的工廠函數建立 module
對象,以後讀入 module
對應的文件內容,調用 loader-runner
對內容作轉化,轉化結果如有其它依賴則繼續讀入依賴資源,重複此過程直到全部依賴均被轉化爲 module
compilation.seal
階段:
module
集合,根據 entry
配置及引入資源的方式,將 module
分配到不一樣的 chunk
chunk
集合,調用 compilation.emitAsset
方法標記 chunk
的輸出規則,即轉化爲 assets
集合compiler.emitAssets
階段:
assets
寫入文件系統網上很多資料將 webpack 的插件架構歸類爲「事件/訂閱」模式,我認爲這種概括有失偏頗。訂閱模式是一種鬆耦合架構,發佈器只是在特定時機發布事件消息,訂閱者並不或者不多與事件直接發生交互,舉例來講,咱們日常在使用 HTML 事件的時候不少時候只是在這個時機觸發業務邏輯,不多調用上下文操做。而 webpack 的鉤子體系是一種強耦合架構,它在特定時機觸發鉤子時會附帶上足夠的上下文信息,插件定義的鉤子回調中,能也只能與這些上下文背後的數據結構、接口交互產生 side effect,進而影響到編譯狀態和後續流程。
學習插件架構,須要理解三個關鍵問題:
從形態上看,插件一般是一個帶有 apply
函數的類:
class SomePlugin { apply(compiler) { } }
apply
函數運行時會獲得參數 compiler
,以此爲起點能夠調用 hook
對象註冊各類鉤子回調,例如: compiler.hooks.make.tapAsync
,這裏面 make
是鉤子名稱,tapAsync
定義了鉤子的調用方式,webpack 的插件架構基於這種模式構建而成,插件開發者可使用這種模式在鉤子回調中,插入特定代碼。webpack 各類內置對象都帶有 hooks
屬性,好比 compilation
對象:
class SomePlugin { apply(compiler) { compiler.hooks.thisCompilation.tap('SomePlugin', (compilation) => { compilation.hooks.optimizeChunkAssets.tapAsync('SomePlugin', ()=>{}); }) } }
鉤子的核心邏輯定義在 Tapable 倉庫,內部定義了以下類型的鉤子:
const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable");
不一樣類型的鉤子根據其並行度、熔斷方式、同步異步,調用方式會略有不一樣,插件開發者須要根據這些的特性,編寫不一樣的交互邏輯,這部份內容也特別多,回頭展開聊聊。
瞭解 webpack 插件的基本形態以後,接下來須要弄清楚一個問題:webpack 會在什麼時間節點觸發什麼鉤子?這一塊我認爲是知識量最大的一部分,畢竟源碼裏面有237個鉤子,但官網只介紹了不到100個,且官網對每一個鉤子的說明都太簡短,就我我的而言看完並無太大收穫,因此有必要展開聊一下這個話題。先看幾個例子:
compiler.hooks.compilation
:
compiler.hooks.make
:
compilation
對象EntryPlugin
基於此鉤子實現 entry
模塊的初始化compilation.hooks.optimizeChunks
:
seal
函數中,chunk
集合構建完畢後觸發chunks
集合與 chunkGroups
集合SplitChunksPlugin
插件基於此鉤子實現 chunk
拆分優化compiler.hooks.done
:
stats
對象,包含編譯過程當中的各種統計信息webpack-bundle-analyzer
插件基於此鉤子實現打包分析這是我總結的鉤子的三個學習要素:觸發時機、傳遞參數、示例代碼。
觸發時機與 webpack 工做過程緊密相關,大致上從啓動到結束,compiler
對象逐次觸發以下鉤子:
而 compilation
對象逐次觸發:
因此,理解清楚前面說的 webpack 工做的主流程,基本上就能夠捋清楚「何時會觸發什麼鉤子」。
傳遞參數與具體的鉤子強相關,官網對這方面沒有作出進一步解釋,個人作法是直接在源碼裏面搜索調用語句,例如對於 compilation.hooks.optimizeTree
,能夠在 webpack 源碼中搜索 hooks.optimizeTree.call
關鍵字,就能夠找到調用代碼:
// lib/compilation.js#2297 this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => { });
結合代碼所在的上下文,能夠判斷出此時傳遞的是通過優化的 chunks
及 modules
集合。
Webpack 的鉤子複雜程度不一,我認爲最好的學習方法仍是帶着目的去查詢其餘插件中如何使用這些鉤子。例如,在 compilation.seal
函數內部有 optimizeModules
和 afterOptimizeModules
這一對看起來很對偶的鉤子,optimizeModules
從字面上能夠理解爲用於優化已經編譯出的 modules
,那 afterOptimizeModules
呢?
從 webpack 源碼中惟一搜索到的用途是 ProgressPlugin
,大致上邏輯以下:
compilation.hooks.afterOptimizeModules.intercept({ name: "ProgressPlugin", call() { handler(percentage, "sealing", title); }, done() { progressReporters.set(compiler, undefined); handler(percentage, "sealing", title); }, result() { handler(percentage, "sealing", title); }, error() { handler(percentage, "sealing", title); }, tap(tap) { // p is percentage from 0 to 1 // args is any number of messages in a hierarchical matter progressReporters.set(compilation.compiler, (p, ...args) => { handler(percentage, "sealing", title, tap.name, ...args); }); handler(percentage, "sealing", title, tap.name); } });
基本上能夠猜想出,afterOptimizeModules
的設計初衷就是用於通知優化行爲的結束。
apply
雖然是一個函數,可是從設計上就只有輸入,webpack 不 care 輸出,因此在插件中只能經過調用類型實體的各類方法來或者更改實體的配置信息,變動編譯行爲。例如:
到這裏,插件的工做機理和寫法已經有一個很粗淺的介紹了,回頭單拎出來細講吧。
解決上述兩個問題以後,咱們就能理解「如何將特定邏輯插入 webpack 編譯過程」,接下來纔是重點 —— 如何影響編譯狀態?強調一下,webpack 的插件體系與日常所見的 訂閱/發佈 模式差異很大,是一種很是強耦合的設計,hooks 回調由 webpack 決定什麼時候,以何種方式執行;而在 hooks 回調內部能夠經過修改狀態、調用上下文 api 等方式對 webpack 產生 side effect。
好比,EntryPlugin
插件:
class EntryPlugin { apply(compiler) { compiler.hooks.compilation.tap( "EntryPlugin", (compilation, { normalModuleFactory }) => { compilation.dependencyFactories.set( EntryDependency, normalModuleFactory ); } ); compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => { const { entry, options, context } = this; const dep = EntryPlugin.createDependency(entry, options); compilation.addEntry(context, dep, options, (err) => { callback(err); }); }); } }
上述代碼片斷調用了兩個影響 compilation
對象狀態的接口:
compilation.dependencyFactories.set
compilation.addEntry
操做的具體含義能夠先忽略,這裏要理解的重點是,webpack 會將上下文信息以參數或 this
(compiler 對象) 形式傳遞給鉤子回調,在回調中能夠調用上下文對象的方法或者直接修改上下文對象屬性的方式,對原定的流程產生 side effect。因此想純熟地編寫插件,除了要理解調用時機,還須要瞭解咱們能夠用哪一些api,例如:
compilation.addModule
:添加模塊,能夠在原有的 module
構建規則以外,添加自定義模塊compilation.emitAsset
:直譯是「提交資產」,功能能夠理解將內容寫入到特定路徑compilation.addEntry
:添加入口,功能上與直接定義 entry
配置相同module.addError
:添加編譯錯誤信息Loader 的做用和實現比較簡單,容易理解,因此簡單介紹一下就好了。回顧 loader 在編譯流程中的生效的位置:
流程圖中, runLoaders
會調用用戶所配置的 loader 集合讀取、轉譯資源,此前的內容能夠千奇百怪,但轉譯以後理論上應該輸出標準 JavaScript 文本或者 AST 對象,webpack 才能繼續處理模塊依賴。
理解了這個基本邏輯以後,loader 的職責就比較清晰了,不外乎是將內容 A 轉化爲內容 B,可是在具體用法層面還挺多講究的,有 pitch、pre、post、inline 等概念用於應對各類場景。
爲了幫助理解,這裏補充一個示例: Webpack 案例 -- vue-loader 原理分析。
ndb
單點調試功能追蹤程序的運行,雖然 node 的調試有不少種方法,可是我我的更推薦 ndb
,靈活、簡單,配合 debugger
語句是大殺器compiler + compilation + plugins
,webpack 運行過程當中只會有一個 compiler
;而每次編譯 —— 包括調用 compiler.run
函數或者 watch = true
時文件發生變動,都會建立一個 compilation
對象。理解這三個核心對象的設計、職責、協做,差很少就能理解 webpack 的核心邏輯了抓大放小: plugin 的關鍵是「鉤子」,我建議戰略上重視,戰術上忽視!鉤子畢竟是 webpack 的關鍵概念,是整個插件機制的根基,學習 webpack 根本不可能繞過鉤子,可是相應的邏輯跳轉實在太繞太不直觀了,看代碼的時候一直揪着這個點的話,複雜性會劇增,個人經驗是:
tapable
的源碼,理解同步鉤子、異步鉤子、promise 鉤子、串行鉤子、並行鉤子等概念,對 tapable
提供的事件模型有一個較爲精細的認知,這叫戰略上重視debugger
語句單點調試,等你縷清後續邏輯的時候,大機率你也知道鉤子的含義了,這叫戰術上忽視保持好奇心:學習過程保持旺盛的好奇心和韌性,善於 \& 勇於提出問題,而後基於源碼和社區資料去總結出本身的答案,問題可能會不少,好比:
compilation.seal
函數內部設計了不少優化型的鉤子,爲何須要區分的這麼細?webpack 設計者對不一樣鉤子有什麼預期?module
子類?這些子類分別在何時被使用?Module
與 Module
子類從上文能夠看出,webpack 構建階段的核心流程基本上都圍繞着 module
展開,相信接觸過、用過 Webpack 的讀者對 module
應該已經有一個感性認知,可是實現上 module
的邏輯是很是複雜繁重的。
以 webpack\@5.26.3 爲例,直接或間接繼承自 Module
(webpack/lib/Module.js
文件) 的子類有54個:
沒法複製加載中的內容
要一個一個捋清楚這些類的做用實在太累了,咱們須要抓住本質:module
的做用是什麼?
module
是 webpack 資源處理的基本單位,能夠認爲 webpack 對資源的路徑解析、讀入、轉譯、分析、打包輸出,全部操做都是圍繞着 module 展開的。有不少文章會說 module = 文件, 其實這種說法並不許確,好比子類 AsyncModuleRuntimeModule
就只是一段內置的代碼,是一種資源而不能簡單等價於實際文件。
Webpack 擴展性很強,包括模塊的處理邏輯上,好比說入口文件是一個普通的 js,此時首先建立 NormalModule 對象,在解析 AST 時發現這個文件裏還包含了異步加載語句,例如 requere.ensure
,那麼相應地會建立 AsyncModuleRuntimeModule
模塊,注入異步加載的模板代碼。上面類圖的 54 個 module 子類都是爲適配各類場景設計的。