<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          萬(wàn)字總結(jié)一文徹底吃透 Webpack 核心原理

          共 25858字,需瀏覽 52分鐘

           ·

          2021-05-05 10:36

          背景

          Webpack 特別難學(xué)!!!時(shí)至 5.0 版本之后,Webpack 功能集變得非常龐大,包括:模塊打包、代碼分割、按需加載、HMR、Tree-shaking、文件監(jiān)聽(tīng)、sourcemap、Module Federation、devServer、DLL、多進(jìn)程等等,為了實(shí)現(xiàn)這些功能,webpack 的代碼量已經(jīng)到了驚人的程度:

          • 498 份JS文件
          • 18862 行注釋
          • 73548 行代碼
          • 54 個(gè) module 類型
          • 69 個(gè) dependency 類型
          • 162 個(gè)內(nèi)置插件
          • 237 個(gè)hook

          在這個(gè)數(shù)量級(jí)下,源碼的閱讀、分析、學(xué)習(xí)成本非常高,加上 webpack 官網(wǎng)語(yǔ)焉不詳?shù)奈臋n,導(dǎo)致 webpack 的學(xué)習(xí)、上手成本極其高。為此,社區(qū)圍繞著 Webpack 衍生出了各種手腳架,比如 vue-cli、create-react-app,解決“用”的問(wèn)題。但這又導(dǎo)致一個(gè)新的問(wèn)題,大部分人在工程化方面逐漸變成一個(gè)配置工程師,停留在“會(huì)用會(huì)配”但是不知道黑盒里面到底是怎么轉(zhuǎn)的階段,遇到具體問(wèn)題就瞎了:

          • 想給基礎(chǔ)庫(kù)做個(gè)升級(jí),出現(xiàn)兼容性問(wèn)題跑不動(dòng)了,直接放棄
          • 想優(yōu)化一下編譯性能,但是不清楚內(nèi)部原理,無(wú)從下手

          究其原因還是對(duì) webpack 內(nèi)部運(yùn)行機(jī)制沒(méi)有形成必要的整體認(rèn)知,無(wú)法迅速定位問(wèn)題 —— 對(duì),連問(wèn)題的本質(zhì)都常常看不出,所謂的不能透過(guò)現(xiàn)象看本質(zhì),那本質(zhì)是啥?我個(gè)人將 webpack 整個(gè)龐大的體系抽象為三方面的知識(shí):

          1. 構(gòu)建的核心流程
          2. loader 的作用
          3. plugin 架構(gòu)與常用套路

          三者協(xié)作構(gòu)成 webpack 的主體框架:

          理解了這三塊內(nèi)容就算是入了個(gè)門,對(duì) Webpack 有了一個(gè)最最基礎(chǔ)的認(rèn)知了,工作中再遇到問(wèn)題也就能按圖索驥了。補(bǔ)充一句,作為一份入門教程,本文不會(huì)展開(kāi)太多 webpack 代碼層面的細(xì)節(jié) —— 我的精力也不允許,所以讀者也不需要看到一堆文字就產(chǎn)生特別大的心理負(fù)擔(dān)。

          核心流程解析

          首先,我們要理解一個(gè)點(diǎn),Webpack 最核心的功能:

          At its core, webpack is a static module bundler for modern JavaScript applications.

          也就是將各種類型的資源,包括圖片、css、js等,轉(zhuǎn)譯、組合、拼接、生成 JS 格式的 bundler 文件。官網(wǎng)首頁(yè)的動(dòng)畫很形象地表達(dá)了這一點(diǎn):

          1d66a833-2841-4a8a-a91a-0da800fab306.png

          這個(gè)過(guò)程核心完成了 內(nèi)容轉(zhuǎn)換 + 資源合并 兩種功能,實(shí)現(xiàn)上包含三個(gè)階段:

          1. 初始化階段:

          2. 初始化參數(shù):從配置文件、 配置對(duì)象、Shell 參數(shù)中讀取,與默認(rèn)配置結(jié)合得出最終的參數(shù)

          3. 創(chuàng)建編譯器對(duì)象:用上一步得到的參數(shù)創(chuàng)建 Compiler 對(duì)象

          4. 初始化編譯環(huán)境:包括注入內(nèi)置插件、注冊(cè)各種模塊工廠、初始化 RuleSet 集合、加載配置的插件等

          5. 開(kāi)始編譯:執(zhí)行 compiler 對(duì)象的 run 方法

          6. 確定入口:根據(jù)配置中的 entry 找出所有的入口文件,調(diào)用 compilition.addEntry將入口文件轉(zhuǎn)換為 dependence 對(duì)象

          7. 構(gòu)建階段:

          8. 編譯模塊(make):根據(jù) entry 對(duì)應(yīng)的 dependence 創(chuàng)建 module 對(duì)象,調(diào)用loader 將模塊轉(zhuǎn)譯為標(biāo)準(zhǔn) JS 內(nèi)容,調(diào)用 JS 解釋器將內(nèi)容轉(zhuǎn)換為 AST 對(duì)象,從中找出該模塊依賴的模塊,再 遞歸 本步驟直到所有入口依賴的文件都經(jīng)過(guò)了本步驟的處理

          9. 完成模塊編譯:上一步遞歸處理所有能觸達(dá)到的模塊后,得到了每個(gè)模塊被翻譯后的內(nèi)容以及它們之間的 依賴關(guān)系圖

          10. 生成階段:

          11. 輸出資源(seal):根據(jù)入口和模塊之間的依賴關(guān)系,組裝成一個(gè)個(gè)包含多個(gè)模塊的 Chunk,再把每個(gè) Chunk 轉(zhuǎn)換成一個(gè)單獨(dú)的文件加入到輸出列表,這步是可以修改輸出內(nèi)容的最后機(jī)會(huì)

          12. 寫入文件系統(tǒng)(emitAssets):在確定好輸出內(nèi)容后,根據(jù)配置確定輸出的路徑和文件名,把文件內(nèi)容寫入到文件系統(tǒng)

          單次構(gòu)建過(guò)程自上而下按順序執(zhí)行,下面會(huì)展開(kāi)聊聊細(xì)節(jié),在此之前,對(duì)上述提及的各類技術(shù)名詞不太熟悉的同學(xué),可以先看看簡(jiǎn)介:

          • Entry:編譯入口,webpack 編譯的起點(diǎn)
          • Compiler:編譯管理器,webpack 啟動(dòng)后會(huì)創(chuàng)建 compiler 對(duì)象,該對(duì)象一直存活知道結(jié)束退出
          • Compilation:?jiǎn)未尉庉嬤^(guò)程的管理器,比如 watch = true 時(shí),運(yùn)行過(guò)程中只有一個(gè) compiler 但每次文件變更觸發(fā)重新編譯時(shí),都會(huì)創(chuàng)建一個(gè)新的 compilation 對(duì)象
          • Dependence:依賴對(duì)象,webpack 基于該類型記錄模塊間依賴關(guān)系
          • Module:webpack 內(nèi)部所有資源都會(huì)以“module”對(duì)象形式存在,所有關(guān)于資源的操作、轉(zhuǎn)譯、合并都是以 “module” 為基本單位進(jìn)行的
          • Chunk:編譯完成準(zhǔn)備輸出時(shí),webpack 會(huì)將 module 按特定的規(guī)則組織成一個(gè)一個(gè)的 chunk,這些 chunk 某種程度上跟最終輸出一一對(duì)應(yīng)
          • Loader:資源內(nèi)容轉(zhuǎn)換器,其實(shí)就是實(shí)現(xiàn)從內(nèi)容 A 轉(zhuǎn)換 B 的轉(zhuǎn)換器
          • Plugin:webpack構(gòu)建過(guò)程中,會(huì)在特定的時(shí)機(jī)廣播對(duì)應(yīng)的事件,插件監(jiān)聽(tīng)這些事件,在特定時(shí)間點(diǎn)介入編譯過(guò)程

          webpack 編譯過(guò)程都是圍繞著這些關(guān)鍵對(duì)象展開(kāi)的,更詳細(xì)完整的信息,可以參考 Webpack 知識(shí)圖譜 。

          初始化階段

          學(xué)習(xí)一個(gè)項(xiàng)目的源碼通常都是從入口開(kāi)始看起,按圖索驥慢慢摸索出套路的,所以先來(lái)看看 webpack 的初始化過(guò)程:

          解釋一下:

          1. 將 process.args + webpack.config.js 合并成用戶配置
          2. 調(diào)用 validateSchema 校驗(yàn)配置
          3. 調(diào)用 getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults 合并出最終配置
          4. 創(chuàng)建 compiler 對(duì)象
          5. 遍歷用戶定義的 plugins 集合,執(zhí)行插件的 apply 方法
          6. 調(diào)用 new WebpackOptionsApply().process 方法,加載各種內(nèi)置插件

          主要邏輯集中在 WebpackOptionsApply 類,webpack 內(nèi)置了數(shù)百個(gè)插件,這些插件并不需要我們手動(dòng)配置,WebpackOptionsApply 會(huì)在初始化階段根據(jù)配置內(nèi)容動(dòng)態(tài)注入對(duì)應(yīng)的插件,包括:

          • 注入 EntryOptionPlugin 插件,處理 entry 配置
          • 根據(jù) devtool 值判斷后續(xù)用那個(gè)插件處理 sourcemap,可選值:EvalSourceMapDevToolPluginSourceMapDevToolPluginEvalDevToolModulePlugin
          • 注入 RuntimePlugin ,用于根據(jù)代碼內(nèi)容動(dòng)態(tài)注入 webpack 運(yùn)行時(shí)

          到這里,compiler 實(shí)例就被創(chuàng)建出來(lái)了,相應(yīng)的環(huán)境參數(shù)也預(yù)設(shè)好了,緊接著開(kāi)始調(diào)用 compiler.compile 函數(shù):

          // 取自 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 架構(gòu)很靈活,但代價(jià)是犧牲了源碼的直觀性,比如說(shuō)上面說(shuō)的初始化流程,從創(chuàng)建compiler 實(shí)例到調(diào)用 make 鉤子,邏輯鏈路很長(zhǎng):

          • 啟動(dòng) webpack ,觸發(fā) lib/webpack.js 文件中 createCompiler 方法
          • createCompiler 方法內(nèi)部調(diào)用 WebpackOptionsApply 插件
          • WebpackOptionsApply 定義在 lib/WebpackOptionsApply.js 文件,內(nèi)部根據(jù) entry 配置決定注入 entry 相關(guān)的插件,包括:DllEntryPluginDynamicEntryPluginEntryPluginPrefetchPluginProgressPluginContainerPlugin
          • Entry 相關(guān)插件,如 lib/EntryPlugin.js 的 EntryPlugin 監(jiān)聽(tīng) compiler.make 鉤子
          • lib/compiler.js 的 compile 函數(shù)內(nèi)調(diào)用 this.hooks.make.callAsync
          • 觸發(fā) EntryPlugin 的 make 回調(diào),在回調(diào)中執(zhí)行 compilation.addEntry 函數(shù)
          • compilation.addEntry 函數(shù)內(nèi)部經(jīng)過(guò)一坨與主流程無(wú)關(guān)的 hook 之后,再調(diào)用 handleModuleCreate 函數(shù),正式開(kāi)始構(gòu)建內(nèi)容

          這個(gè)過(guò)程需要在 webpack 初始化的時(shí)候預(yù)埋下各種插件,經(jīng)歷 4 個(gè)文件,7次跳轉(zhuǎn)才開(kāi)始進(jìn)入主題,前戲太足了,如果讀者對(duì) webpack 的概念、架構(gòu)、組件沒(méi)有足夠了解時(shí),源碼閱讀過(guò)程會(huì)很痛苦。關(guān)于這個(gè)問(wèn)題,我在文章最后總結(jié)了一些技巧和建議,有興趣的可以滑到附錄閱讀模塊。

          構(gòu)建階段

          基本流程

          你有沒(méi)有思考過(guò)這樣的問(wèn)題:

          • Webpack 編譯過(guò)程會(huì)將源碼解析為 AST 嗎?webpack 與 babel 分別實(shí)現(xiàn)了什么?
          • Webpack 編譯過(guò)程中,如何識(shí)別資源對(duì)其他資源的依賴?
          • 相對(duì)于 grunt、gulp 等流式構(gòu)建工具,為什么 webpack 會(huì)被認(rèn)為是新一代的構(gòu)建工具?

          這些問(wèn)題,基本上在構(gòu)建階段都能看出一些端倪。構(gòu)建階段從 entry 開(kāi)始遞歸解析資源與資源的依賴,在 compilation 對(duì)象內(nèi)逐步構(gòu)建出 module 集合以及 module 之間的依賴關(guān)系,核心流程:

          解釋一下,構(gòu)建階段從入口文件開(kāi)始:

          1. 調(diào)用 handleModuleCreate ,根據(jù)文件類型構(gòu)建 module 子類

          2. 調(diào)用 loader-runner 倉(cāng)庫(kù)的 runLoaders 轉(zhuǎn)譯 module 內(nèi)容,通常是從各類資源類型轉(zhuǎn)譯為 JavaScript 文本

          3. 調(diào)用 acorn 將 JS 文本解析為AST

          4. 遍歷 AST,觸發(fā)各種鉤子

          5. 在 HarmonyExportDependencyParserPlugin 插件監(jiān)聽(tīng) exportImportSpecifier 鉤子,解讀 JS 文本對(duì)應(yīng)的資源依賴

          6. 調(diào)用 module 對(duì)象的 addDependency 將依賴對(duì)象加入到 module 依賴列表中

          7. AST 遍歷完畢后,調(diào)用 module.handleParseResult 處理模塊依賴

          8. 對(duì)于 module 新增的依賴,調(diào)用 handleModuleCreate ,控制流回到第一步

          9. 所有依賴都解析完畢后,構(gòu)建階段結(jié)束

          這個(gè)過(guò)程中數(shù)據(jù)流 module => ast => dependences => module ,先轉(zhuǎn) AST 再?gòu)?AST 找依賴。這就要求 loaders 處理完的最后結(jié)果必須是可以被 acorn 處理的標(biāo)準(zhǔn) JavaScript 語(yǔ)法,比如說(shuō)對(duì)于圖片,需要從圖像二進(jìn)制轉(zhuǎn)換成類似于 export default "data:image/png;base64,xxx" 這類 base64 格式或者 export default "http://xxx" 這類 url 格式。compilation 按這個(gè)流程遞歸處理,逐步解析出每個(gè)模塊的內(nèi)容以及 module 依賴關(guān)系,后續(xù)就可以根據(jù)這些內(nèi)容打包輸出。

          示例:層級(jí)遞進(jìn)

          假如有如下圖所示的文件依賴樹(shù):

          其中 index.js 為 entry 文件,依賴于 a/b 文件;a 依賴于 c/d 文件。初始化編譯環(huán)境之后,EntryPlugin 根據(jù) entry 配置找到 index.js 文件,調(diào)用 compilation.addEntry 函數(shù)觸發(fā)構(gòu)建流程,構(gòu)建完畢后內(nèi)部會(huì)生成這樣的數(shù)據(jù)結(jié)構(gòu):

          此時(shí)得到 module[index.js] 的內(nèi)容以及對(duì)應(yīng)的依賴對(duì)象 dependence[a.js] 、dependence[b.js] 。OK,這就得到下一步的線索:a.js、b.js,根據(jù)上面流程圖的邏輯繼續(xù)調(diào)用 module[index.js] 的 handleParseResult 函數(shù),繼續(xù)處理 a.js、b.js 文件,遞歸上述流程,進(jìn)一步得到 a、b 模塊:

          從 a.js 模塊中又解析到 c.js/d.js 依賴,于是再再繼續(xù)調(diào)用 module[a.js] 的handleParseResult ,再再遞歸上述流程:

          到這里解析完所有模塊后,發(fā)現(xiàn)沒(méi)有更多新的依賴,就可以繼續(xù)推進(jìn),進(jìn)入下一步。

          總結(jié)

          回顧章節(jié)開(kāi)始時(shí)提到的問(wèn)題:

          • Webpack 編譯過(guò)程會(huì)將源碼解析為 AST 嗎?webpack 與 babel 分別實(shí)現(xiàn)了什么?

            • 構(gòu)建階段會(huì)讀取源碼,解析為 AST 集合。
            • Webpack 讀出 AST 之后僅遍歷 AST 集合;babel 則對(duì)源碼做等價(jià)轉(zhuǎn)換
          • Webpack 編譯過(guò)程中,如何識(shí)別資源對(duì)其他資源的依賴?

            • Webpack 遍歷 AST 集合過(guò)程中,識(shí)別 require/ import 之類的導(dǎo)入語(yǔ)句,確定模塊對(duì)其他資源的依賴關(guān)系
          • 相對(duì)于 grant、gulp 等流式構(gòu)建工具,為什么 webpack 會(huì)被認(rèn)為是新一代的構(gòu)建工具?

            • Grant、Gulp 僅執(zhí)行開(kāi)發(fā)者預(yù)定義的任務(wù)流;而 webpack 則深入處理資源的內(nèi)容,功能上更強(qiáng)大

          生成階段

          基本流程

          構(gòu)建階段圍繞 module 展開(kāi),生成階段則圍繞 chunks 展開(kāi)。經(jīng)過(guò)構(gòu)建階段之后,webpack 得到足夠的模塊內(nèi)容與模塊關(guān)系信息,接下來(lái)開(kāi)始生成最終資源了。代碼層面,就是開(kāi)始執(zhí)行compilation.seal 函數(shù):

          // 取自 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 原意密封、上鎖,我個(gè)人理解在 webpack 語(yǔ)境下接近于 “將模塊裝進(jìn)蜜罐” 。seal 函數(shù)主要完成從 module 到 chunks 的轉(zhuǎn)化,核心流程:

          簡(jiǎn)單梳理一下:

          1. 構(gòu)建本次編譯的 ChunkGraph 對(duì)象;
          2. 遍歷 compilation.modules 集合,將 module 按 entry/動(dòng)態(tài)引入 的規(guī)則分配給不同的 Chunk 對(duì)象;
          3. compilation.modules 集合遍歷完畢后,得到完整的 chunks 集合對(duì)象,調(diào)用 createXxxAssets 方法
          4. createXxxAssets 遍歷 module/chunk ,調(diào)用 compilation.emitAssets 方法將資assets 信息記錄到 compilation.assets 對(duì)象中
          5. 觸發(fā) seal 回調(diào),控制流回到 compiler 對(duì)象

          這一步的關(guān)鍵邏輯是將 module 按規(guī)則組織成 chunks ,webpack 內(nèi)置的 chunk 封裝規(guī)則比較簡(jiǎn)單:

          • entry 及 entry 觸達(dá)到的模塊,組合成一個(gè) chunk
          • 使用動(dòng)態(tài)引入語(yǔ)句引入的模塊,各自組合成一個(gè) chunk

          chunk 是輸出的基本單位,默認(rèn)情況下這些 chunks 與最終輸出的資源一一對(duì)應(yīng),那按上面的規(guī)則大致上可以推導(dǎo)出一個(gè) entry 會(huì)對(duì)應(yīng)打包出一個(gè)資源,而通過(guò)動(dòng)態(tài)引入語(yǔ)句引入的模塊,也對(duì)應(yīng)會(huì)打包出相應(yīng)的資源,我們來(lái)看個(gè)示例。

          示例:多入口打包

          假如有這樣的配置:

          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"),  
            },  
            devtoolfalse,  
            target"web",  
            plugins: [],  
          };   

          實(shí)例配置中有兩個(gè)入口,對(duì)應(yīng)的文件結(jié)構(gòu):

          index-a 依賴于c,且動(dòng)態(tài)引入了 e;index-b 依賴于 c/d 。根據(jù)上面說(shuō)的規(guī)則:

          • entry 及entry觸達(dá)到的模塊,組合成一個(gè) chunk
          • 使用動(dòng)態(tài)引入語(yǔ)句引入的模塊,各自組合成一個(gè) chunk

          生成的 chunks 結(jié)構(gòu)為:

          也就是根據(jù)依賴關(guān)系,chunk[a] 包含了 index-a/c 兩個(gè)模塊;chunk[b] 包含了 c/index-b/d 三個(gè)模塊;chunk[e-hash] 為動(dòng)態(tài)引入 e 對(duì)應(yīng)的 chunk。不知道大家注意到?jīng)]有,chunk[a] 與 chunk[b] 同時(shí)包含了 c,這個(gè)問(wèn)題放到具體業(yè)務(wù)場(chǎng)景可能就是,一個(gè)多頁(yè)面應(yīng)用,所有頁(yè)面都依賴于相同的基礎(chǔ)庫(kù),那么這些所有頁(yè)面對(duì)應(yīng)的 entry 都會(huì)包含有基礎(chǔ)庫(kù)代碼,這豈不浪費(fèi)?為了解決這個(gè)問(wèn)題,webpack 提供了一些插件如 CommonsChunkPlugin 、SplitChunksPlugin,在基本規(guī)則之外進(jìn)一步優(yōu)化 chunks 結(jié)構(gòu)。

          SplitChunksPlugin 的作用

          SplitChunksPlugin 是 webpack 架構(gòu)高擴(kuò)展的一個(gè)絕好的示例,我們上面說(shuō)了 webpack 主流程里面是按 entry / 動(dòng)態(tài)引入 兩種情況組織 chunks 的,這必然會(huì)引發(fā)一些不必要的重復(fù)打包,webpack 通過(guò)插件的形式解決這個(gè)問(wèn)題。回顧 compilation.seal 函數(shù)的代碼,大致上可以梳理成這么4個(gè)步驟:

          1. 遍歷 compilation.modules ,記錄下模塊與 chunk 關(guān)系
          2. 觸發(fā)各種模塊優(yōu)化鉤子,這一步優(yōu)化的主要是模塊依賴關(guān)系
          3. 遍歷 module 構(gòu)建 chunk 集合
          4. 觸發(fā)各種優(yōu)化鉤子


          上面 1-3 都是預(yù)處理 + chunks 默認(rèn)規(guī)則的實(shí)現(xiàn),不在我們討論范圍,這里重點(diǎn)關(guān)注第4個(gè)步驟觸發(fā)的 optimizeChunks 鉤子,這個(gè)時(shí)候已經(jīng)跑完主流程的邏輯,得到 chunks 集合,SplitChunksPlugin 正是使用這個(gè)鉤子,分析 chunks 集合的內(nèi)容,按配置規(guī)則增加一些通用的 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 插件架構(gòu)的高擴(kuò)展性,使得整個(gè)編譯的主流程是可以固化下來(lái)的,分支邏輯和細(xì)節(jié)需求“外包”出去由第三方實(shí)現(xiàn),這套規(guī)則架設(shè)起了龐大的 webpack 生態(tài),關(guān)于插件架構(gòu)的更多細(xì)節(jié),下面 plugin 部分有詳細(xì)介紹,這里先跳過(guò)。

          寫入文件系統(tǒng)

          經(jīng)過(guò)構(gòu)建階段后,compilation 會(huì)獲知資源模塊的內(nèi)容與依賴關(guān)系,也就知道“輸入”是什么;而經(jīng)過(guò) seal 階段處理后, compilation 則獲知資源輸出的圖譜,也就是知道怎么“輸出”:哪些模塊跟那些模塊“綁定”在一起輸出到哪里。seal 后大致的數(shù)據(jù)結(jié)構(gòu):

          compilation = {  
            // ...  
            modules: [  
              /* ... */  
            ],  
            chunks: [  
              {  
                id"entry name",  
                files: ["output file name"],  
                hash"xxx",  
                runtime"xxx",  
                entryPoint: {xxx}  
                // ...  
              },  
              // ...  
            ],  
          };  

          seal 結(jié)束之后,緊接著調(diào)用 compiler.emitAssets 函數(shù),函數(shù)內(nèi)部調(diào)用 compiler.outputFileSystem.writeFile 方法將 assets 集合寫入文件系統(tǒng),實(shí)現(xiàn)邏輯比較曲折,但是與主流程沒(méi)有太多關(guān)系,所以這里就不展開(kāi)講了。

          資源形態(tài)流轉(zhuǎn)

          OK,上面已經(jīng)把邏輯層面的構(gòu)造主流程梳理完了,這里結(jié)合資源形態(tài)流轉(zhuǎn)的角度重新考察整個(gè)過(guò)程,加深理解:

          • compiler.make 階段:

            • entry 文件以 dependence 對(duì)象形式加入 compilation 的依賴列表,dependence 對(duì)象記錄有 entry 的類型、路徑等信息
            • 根據(jù) dependence 調(diào)用對(duì)應(yīng)的工廠函數(shù)創(chuàng)建 module 對(duì)象,之后讀入 module 對(duì)應(yīng)的文件內(nèi)容,調(diào)用 loader-runner 對(duì)內(nèi)容做轉(zhuǎn)化,轉(zhuǎn)化結(jié)果若有其它依賴則繼續(xù)讀入依賴資源,重復(fù)此過(guò)程直到所有依賴均被轉(zhuǎn)化為 module
          • compilation.seal 階段:

            • 遍歷 module 集合,根據(jù) entry 配置及引入資源的方式,將 module 分配到不同的 chunk
            • 遍歷 chunk 集合,調(diào)用 compilation.emitAsset 方法標(biāo)記 chunk 的輸出規(guī)則,即轉(zhuǎn)化為 assets 集合
          • compiler.emitAssets 階段:

            • 將 assets 寫入文件系統(tǒng)

          Plugin 解析

          網(wǎng)上不少資料將 webpack 的插件架構(gòu)歸類為“事件/訂閱”模式,我認(rèn)為這種歸納有失偏頗。訂閱模式是一種松耦合架構(gòu),發(fā)布器只是在特定時(shí)機(jī)發(fā)布事件消息,訂閱者并不或者很少與事件直接發(fā)生交互,舉例來(lái)說(shuō),我們平常在使用 HTML 事件的時(shí)候很多時(shí)候只是在這個(gè)時(shí)機(jī)觸發(fā)業(yè)務(wù)邏輯,很少調(diào)用上下文操作。而 webpack 的鉤子體系是一種強(qiáng)耦合架構(gòu),它在特定時(shí)機(jī)觸發(fā)鉤子時(shí)會(huì)附帶上足夠的上下文信息,插件定義的鉤子回調(diào)中,能也只能與這些上下文背后的數(shù)據(jù)結(jié)構(gòu)、接口交互產(chǎn)生 side effect,進(jìn)而影響到編譯狀態(tài)和后續(xù)流程。學(xué)習(xí)插件架構(gòu),需要理解三個(gè)關(guān)鍵問(wèn)題:

          • WHAT: 什么是插件
          • WHEN: 什么時(shí)間點(diǎn)會(huì)有什么鉤子被觸發(fā)
          • HOW: 在鉤子回調(diào)中,如何影響編譯狀態(tài)

          What: 什么是插件

          從形態(tài)上看,插件通常是一個(gè)帶有 apply 函數(shù)的類:

          class SomePlugin {  
              apply(compiler) {  
              }  
          }   

          apply 函數(shù)運(yùn)行時(shí)會(huì)得到參數(shù) compiler ,以此為起點(diǎn)可以調(diào)用 hook 對(duì)象注冊(cè)各種鉤子回調(diào),例如:compiler.hooks.make.tapAsync ,這里面 make 是鉤子名稱,tapAsync 定義了鉤子的調(diào)用方式,webpack 的插件架構(gòu)基于這種模式構(gòu)建而成,插件開(kāi)發(fā)者可以使用這種模式在鉤子回調(diào)中,插入特定代碼。webpack 各種內(nèi)置對(duì)象都帶有 hooks 屬性,比如 compilation 對(duì)象:

          class SomePlugin {  
              apply(compiler) {  
                  compiler.hooks.thisCompilation.tap('SomePlugin', (compilation) => {  
                      compilation.hooks.optimizeChunkAssets.tapAsync('SomePlugin', ()=>{});  
                  })  
              }  
          }   

          鉤子的核心邏輯定義在 Tapable 倉(cāng)庫(kù),內(nèi)部定義了如下類型的鉤子:

          const {  
                  SyncHook,  
                  SyncBailHook,  
                  SyncWaterfallHook,  
                  SyncLoopHook,  
                  AsyncParallelHook,  
                  AsyncParallelBailHook,  
                  AsyncSeriesHook,  
                  AsyncSeriesBailHook,  
                  AsyncSeriesWaterfallHook  
           } = require("tapable");   

          不同類型的鉤子根據(jù)其并行度、熔斷方式、同步異步,調(diào)用方式會(huì)略有不同,插件開(kāi)發(fā)者需要根據(jù)這些的特性,編寫不同的交互邏輯,這部分內(nèi)容也特別多,回頭展開(kāi)聊聊。

          When: 什么時(shí)候會(huì)觸發(fā)鉤子

          了解 webpack 插件的基本形態(tài)之后,接下來(lái)需要弄清楚一個(gè)問(wèn)題:webpack 會(huì)在什么時(shí)間節(jié)點(diǎn)觸發(fā)什么鉤子?這一塊我認(rèn)為是知識(shí)量最大的一部分,畢竟源碼里面有237個(gè)鉤子,但官網(wǎng)只介紹了不到100個(gè),且官網(wǎng)對(duì)每個(gè)鉤子的說(shuō)明都太簡(jiǎn)短,就我個(gè)人而言看完并沒(méi)有太大收獲,所以有必要展開(kāi)聊一下這個(gè)話題。先看幾個(gè)例子:

          • compiler.hooks.compilation :

            • 時(shí)機(jī):?jiǎn)?dòng)編譯創(chuàng)建出 compilation 對(duì)象后觸發(fā)
            • 參數(shù):當(dāng)前編譯的 compilation 對(duì)象
            • 示例:很多插件基于此事件獲取 compilation 實(shí)例
          • compiler.hooks.make

            • 時(shí)機(jī):正式開(kāi)始編譯時(shí)觸發(fā)
            • 參數(shù):同樣是當(dāng)前編譯的 compilation 對(duì)象
            • 示例:webpack 內(nèi)置的 EntryPlugin 基于此鉤子實(shí)現(xiàn) entry 模塊的初始化
          • compilation.hooks.optimizeChunks :

            • 時(shí)機(jī):seal 函數(shù)中,chunk 集合構(gòu)建完畢后觸發(fā)
            • 參數(shù):chunks 集合與 chunkGroups 集合
            • 示例:SplitChunksPlugin 插件基于此鉤子實(shí)現(xiàn) chunk 拆分優(yōu)化
          • compiler.hooks.done

            • 時(shí)機(jī):編譯完成后觸發(fā)
            • 參數(shù):stats 對(duì)象,包含編譯過(guò)程中的各類統(tǒng)計(jì)信息
            • 示例:webpack-bundle-analyzer 插件基于此鉤子實(shí)現(xiàn)打包分析

          這是我總結(jié)的鉤子的三個(gè)學(xué)習(xí)要素:觸發(fā)時(shí)機(jī)、傳遞參數(shù)、示例代碼。

          觸發(fā)時(shí)機(jī)

          觸發(fā)時(shí)機(jī)與 webpack 工作過(guò)程緊密相關(guān),大體上從啟動(dòng)到結(jié)束,compiler 對(duì)象逐次觸發(fā)如下鉤子:


          而 compilation 對(duì)象逐次觸發(fā):


          所以,理解清楚前面說(shuō)的 webpack 工作的主流程,基本上就可以捋清楚“什么時(shí)候會(huì)觸發(fā)什么鉤子”。

          參數(shù)

          傳遞參數(shù)與具體的鉤子強(qiáng)相關(guān),官網(wǎng)對(duì)這方面沒(méi)有做出進(jìn)一步解釋,我的做法是直接在源碼里面搜索調(diào)用語(yǔ)句,例如對(duì)于 compilation.hooks.optimizeTree ,可以在 webpack 源碼中搜索hooks.optimizeTree.call 關(guān)鍵字,就可以找到調(diào)用代碼:

          // lib/compilation.js#2297  
          this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {  
          });   

          結(jié)合代碼所在的上下文,可以判斷出此時(shí)傳遞的是經(jīng)過(guò)優(yōu)化的 chunks 及 modules 集合。

          找到示例

          Webpack 的鉤子復(fù)雜程度不一,我認(rèn)為最好的學(xué)習(xí)方法還是帶著目的去查詢其他插件中如何使用這些鉤子。例如,在 compilation.seal 函數(shù)內(nèi)部有 optimizeModules 和 afterOptimizeModules 這一對(duì)看起來(lái)很對(duì)偶的鉤子,optimizeModules 從字面上可以理解為用于優(yōu)化已經(jīng)編譯出的 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);  
            }  
          });   

          基本上可以猜測(cè)出,afterOptimizeModules 的設(shè)計(jì)初衷就是用于通知優(yōu)化行為的結(jié)束。apply 雖然是一個(gè)函數(shù),但是從設(shè)計(jì)上就只有輸入,webpack 不 care 輸出,所以在插件中只能通過(guò)調(diào)用類型實(shí)體的各種方法來(lái)或者更改實(shí)體的配置信息,變更編譯行為。例如:

          • compilation.addModule :添加模塊,可以在原有的 module 構(gòu)建規(guī)則之外,添加自定義模塊
          • compilation.emitAsset:直譯是“提交資產(chǎn)”,功能可以理解將內(nèi)容寫入到特定路徑

          到這里,插件的工作機(jī)理和寫法已經(jīng)有一個(gè)很粗淺的介紹了,回頭單拎出來(lái)細(xì)講吧。

          How: 如何影響編譯狀態(tài)

          解決上述兩個(gè)問(wèn)題之后,我們就能理解“如何將特定邏輯插入 webpack 編譯過(guò)程”,接下來(lái)才是重點(diǎn) —— 如何影響編譯狀態(tài)?強(qiáng)調(diào)一下,webpack 的插件體系與平常所見(jiàn)的 訂閱/發(fā)布 模式差別很大,是一種非常強(qiáng)耦合的設(shè)計(jì),hooks 回調(diào)由 webpack 決定何時(shí),以何種方式執(zhí)行;而在 hooks 回調(diào)內(nèi)部可以通過(guò)修改狀態(tài)、調(diào)用上下文 api 等方式對(duì) webpack 產(chǎn)生 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);  
                });  
              });  
            }  
          }  

          上述代碼片段調(diào)用了兩個(gè)影響 compilation 對(duì)象狀態(tài)的接口:

          • compilation.dependencyFactories.set
          • compilation.addEntry

          操作的具體含義可以先忽略,這里要理解的重點(diǎn)是,webpack 會(huì)將上下文信息以參數(shù)或 this(compiler 對(duì)象) 形式傳遞給鉤子回調(diào),在回調(diào)中可以調(diào)用上下文對(duì)象的方法或者直接修改上下文對(duì)象屬性的方式,對(duì)原定的流程產(chǎn)生 side effect。所以想純熟地編寫插件,除了要理解調(diào)用時(shí)機(jī),還需要了解我們可以用哪一些api,例如:

          • compilation.addModule:添加模塊,可以在原有的 module 構(gòu)建規(guī)則之外,添加自定義模塊
          • compilation.emitAsset:直譯是“提交資產(chǎn)”,功能可以理解將內(nèi)容寫入到特定路徑
          • compilation.addEntry:添加入口,功能上與直接定義 entry 配置相同
          • module.addError:添加編譯錯(cuò)誤信息
          • ...

          Loader 介紹

          Loader 的作用和實(shí)現(xiàn)比較簡(jiǎn)單,容易理解,所以簡(jiǎn)單介紹一下就行了。回顧 loader 在編譯流程中的生效的位置:

          流程圖中, runLoaders 會(huì)調(diào)用用戶所配置的 loader 集合讀取、轉(zhuǎn)譯資源,此前的內(nèi)容可以千奇百怪,但轉(zhuǎn)譯之后理論上應(yīng)該輸出標(biāo)準(zhǔn) JavaScript 文本或者 AST 對(duì)象,webpack 才能繼續(xù)處理模塊依賴。理解了這個(gè)基本邏輯之后,loader 的職責(zé)就比較清晰了,不外乎是將內(nèi)容 A 轉(zhuǎn)化為內(nèi)容 B,但是在具體用法層面還挺多講究的,有 pitch、pre、post、inline 等概念用于應(yīng)對(duì)各種場(chǎng)景。為了幫助理解,這里補(bǔ)充一個(gè)示例:Webpack 案例 -- vue-loader 原理分析。

          附錄

          源碼閱讀技巧

          • 避重就輕: 挑軟柿子捏,比如初始化過(guò)程雖然繞,但是相對(duì)來(lái)說(shuō)是概念最少、邏輯最清晰的,那從這里入手摸清整個(gè)工作過(guò)程,可以習(xí)得 webpack 的一些通用套路,例如鉤子的設(shè)計(jì)與作用、編碼規(guī)則、命名習(xí)慣、內(nèi)置插件的加載邏輯等,相當(dāng)于先入了個(gè)門

          • 學(xué)會(huì)調(diào)試: 多用 ndb 單點(diǎn)調(diào)試功能追蹤程序的運(yùn)行,雖然 node 的調(diào)試有很多種方法,但是我個(gè)人更推薦 ndb ,靈活、簡(jiǎn)單,配合 debugger 語(yǔ)句是大殺器

          • 理解架構(gòu): 某種程度上可以將 webpack 架構(gòu)簡(jiǎn)化為 compiler + compilation + plugins,webpack 運(yùn)行過(guò)程中只會(huì)有一個(gè) compiler ;而每次編譯 —— 包括調(diào)用 compiler.run函數(shù)或者 watch = true 時(shí)文件發(fā)生變更,都會(huì)創(chuàng)建一個(gè) compilation 對(duì)象。理解這三個(gè)核心對(duì)象的設(shè)計(jì)、職責(zé)、協(xié)作,差不多就能理解 webpack 的核心邏輯了

          • 抓大放小: plugin 的關(guān)鍵是“鉤子”,我建議戰(zhàn)略上重視,戰(zhàn)術(shù)上忽視!鉤子畢竟是 webpack 的關(guān)鍵概念,是整個(gè)插件機(jī)制的根基,學(xué)習(xí) webpack 根本不可能繞過(guò)鉤子,但是相應(yīng)的邏輯跳轉(zhuǎn)實(shí)在太繞太不直觀了,看代碼的時(shí)候一直揪著這個(gè)點(diǎn)的話,復(fù)雜性會(huì)劇增,我的經(jīng)驗(yàn)是:

            • 認(rèn)真看一下 tapable 倉(cāng)庫(kù)的文檔,或者粗略看一下 tapable 的源碼,理解同步鉤子、異步鉤子、promise 鉤子、串行鉤子、并行鉤子等概念,對(duì) tapable 提供的事件模型有一個(gè)較為精細(xì)的認(rèn)知,這叫戰(zhàn)略上重視
            • 遇到不懂的鉤子別慌,我的經(jīng)驗(yàn)我連這個(gè)類都不清楚干啥的,要去理解這些鉤子實(shí)在太難了,不如先略過(guò)鉤子本身的含義,去看那些插件用到了它,然后到插件哪里去加debugger 語(yǔ)句單點(diǎn)調(diào)試,等你縷清后續(xù)邏輯的時(shí)候,大概率你也知道鉤子的含義了,這叫戰(zhàn)術(shù)上忽視
          • 保持好奇心: 學(xué)習(xí)過(guò)程保持旺盛的好奇心和韌性,善于 & 敢于提出問(wèn)題,然后基于源碼和社區(qū)資料去總結(jié)出自己的答案,問(wèn)題可能會(huì)很多,比如:

            • loader 為什么要設(shè)計(jì) pre、pitch、post、inline?
            • compilation.seal 函數(shù)內(nèi)部設(shè)計(jì)了很多優(yōu)化型的鉤子,為什么需要區(qū)分的這么細(xì)?webpack 設(shè)計(jì)者對(duì)不同鉤子有什么預(yù)期?
            • 為什么需要那么多 module 子類?這些子類分別在什么時(shí)候被使用?

          Module 與 Module 子類

          從上文可以看出,webpack 構(gòu)建階段的核心流程基本上都圍繞著 module 展開(kāi),相信接觸過(guò)、用過(guò) Webpack 的讀者對(duì) module 應(yīng)該已經(jīng)有一個(gè)感性認(rèn)知,但是實(shí)現(xiàn)上 module 的邏輯是非常復(fù)雜繁重的。以 [email protected] 為例,直接或間接繼承自 Module (webpack/lib/Module.js 文件) 的子類有54個(gè):

          module 體系.png

          要一個(gè)一個(gè)捋清楚這些類的作用實(shí)在太累了,我們需要抓住本質(zhì):module 的作用是什么?module 是 webpack 資源處理的基本單位,可以認(rèn)為 webpack 對(duì)資源的路徑解析、讀入、轉(zhuǎn)譯、分析、打包輸出,所有操作都是圍繞著 module 展開(kāi)的。有很多文章會(huì)說(shuō) module = 文件, 其實(shí)這種說(shuō)法并不準(zhǔn)確,比如子類 AsyncModuleRuntimeModule 就只是一段內(nèi)置的代碼,是一種資源而不能簡(jiǎn)單等價(jià)于實(shí)際文件。Webpack 擴(kuò)展性很強(qiáng),包括模塊的處理邏輯上,比如說(shuō)入口文件是一個(gè)普通的 js,此時(shí)首先創(chuàng)建 NormalModule 對(duì)象,在解析 AST 時(shí)發(fā)現(xiàn)這個(gè)文件里還包含了異步加載語(yǔ)句,例如 requere.ensure ,那么相應(yīng)地會(huì)創(chuàng)建 AsyncModuleRuntimeModule 模塊,注入異步加載的模板代碼。上面類圖的 54 個(gè) module 子類都是為適配各種場(chǎng)景設(shè)計(jì)的。

          • 本文作者:Tecvan

          • 本文鏈接:https://juejin.cn/post/6949040393165996040


          瀏覽 36
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久v| 亚洲最大黄网 | 五月丁香在线中文字幕 | 美女被肏视频网站 | 极品另类 |