<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>

          [萬字總結(jié)] 一文吃透 Webpack 核心原理

          共 21484字,需瀏覽 43分鐘

           ·

          2021-09-05 16:38

          背景

          Webpack 特別難學(xué)!!!

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

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

          在這個數(shù)量級下,源碼的閱讀、分析、學(xué)習(xí)成本非常高,加上 webpack 官網(wǎng)語焉不詳?shù)奈臋n,導(dǎo)致 webpack 的學(xué)習(xí)、上手成本極其高。為此,社區(qū)圍繞著 Webpack 衍生出了各種手腳架,比如 vue-cli、create-react-app,解決“用”的問題。

          但這又導(dǎo)致一個新的問題,大部分人在工程化方面逐漸變成一個配置工程師,停留在“會用會配”但是不知道黑盒里面到底是怎么轉(zhuǎn)的階段,遇到具體問題就瞎了:

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

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

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

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

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

          核心流程解析

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

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

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

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

          1. 初始化階段:
            1. 初始化參數(shù):從配置文件、 配置對象、Shell 參數(shù)中讀取,與默認(rèn)配置結(jié)合得出最終的參數(shù)
            2. 創(chuàng)建編譯器對象:用上一步得到的參數(shù)創(chuàng)建 Compiler 對象
            3. 初始化編譯環(huán)境:包括注入內(nèi)置插件、注冊各種模塊工廠、初始化 RuleSet 集合、加載配置的插件等
            4. 開始編譯:執(zhí)行 compiler 對象的 run 方法
            5. 確定入口:根據(jù)配置中的 entry 找出所有的入口文件,調(diào)用 compilition.addEntry 將入口文件轉(zhuǎn)換為 dependence 對象
          2. 構(gòu)建階段:
            1. 編譯模塊(make):根據(jù) entry 對應(yīng)的 dependence 創(chuàng)建 module 對象,調(diào)用 loader 將模塊轉(zhuǎn)譯為標(biāo)準(zhǔn) JS 內(nèi)容,調(diào)用 JS 解釋器將內(nèi)容轉(zhuǎn)換為 AST 對象,從中找出該模塊依賴的模塊,再 遞歸 本步驟直到所有入口依賴的文件都經(jīng)過了本步驟的處理
            2. 完成模塊編譯:上一步遞歸處理所有能觸達(dá)到的模塊后,得到了每個模塊被翻譯后的內(nèi)容以及它們之間的 依賴關(guān)系圖
          3. 生成階段:
            1. 輸出資源(seal):根據(jù)入口和模塊之間的依賴關(guān)系,組裝成一個個包含多個模塊的 Chunk,再把每個 Chunk 轉(zhuǎn)換成一個單獨(dú)的文件加入到輸出列表,這步是可以修改輸出內(nèi)容的最后機(jī)會
            2. 寫入文件系統(tǒng)(emitAssets):在確定好輸出內(nèi)容后,根據(jù)配置確定輸出的路徑和文件名,把文件內(nèi)容寫入到文件系統(tǒng)

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

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

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

          初始化階段

          基本流程

          學(xué)習(xí)一個項目的源碼通常都是從入口開始看起,按圖索驥慢慢摸索出套路的,所以先來看看 webpack 的初始化過程:

          解釋一下:

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

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

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

          到這里,compiler 實(shí)例就被創(chuàng)建出來了,相應(yīng)的環(huán)境參數(shù)也預(yù)設(shè)好了,緊接著開始調(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)很靈活,但代價是犧牲了源碼的直觀性,比如說上面說的初始化流程,從創(chuàng)建 compiler 實(shí)例到調(diào)用 make 鉤子,邏輯鏈路很長:

          • 啟動 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.jsEntryPlugin 監(jiān)聽 compiler.make 鉤子
          • lib/compiler.jscompile 函數(shù)內(nèi)調(diào)用 this.hooks.make.callAsync
          • 觸發(fā) EntryPluginmake 回調(diào),在回調(diào)中執(zhí)行 compilation.addEntry 函數(shù)
          • compilation.addEntry 函數(shù)內(nèi)部經(jīng)過一坨與主流程無關(guān)的 hook 之后,再調(diào)用 handleModuleCreate 函數(shù),正式開始構(gòu)建內(nèi)容

          這個過程需要在 webpack 初始化的時候預(yù)埋下各種插件,經(jīng)歷 4 個文件,7次跳轉(zhuǎn)才開始進(jìn)入主題,前戲太足了,如果讀者對 webpack 的概念、架構(gòu)、組件沒有足夠了解時,源碼閱讀過程會很痛苦。

          關(guān)于這個問題,我在文章最后總結(jié)了一些技巧和建議,有興趣的可以滑到附錄閱讀模塊。

          構(gòu)建階段

          基本流程

          你有沒有思考過這樣的問題:

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

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

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

          1. 調(diào)用 handleModuleCreate ,根據(jù)文件類型構(gòu)建 module 子類
          2. 調(diào)用 loader-runner 倉庫的 runLoaders 轉(zhuǎn)譯 module 內(nèi)容,通常是從各類資源類型轉(zhuǎn)譯為 JavaScript 文本
          3. 調(diào)用 acorn 將 JS 文本解析為AST
          4. 遍歷 AST,觸發(fā)各種鉤子
            1. HarmonyExportDependencyParserPlugin 插件監(jiān)聽 exportImportSpecifier 鉤子,解讀 JS 文本對應(yīng)的資源依賴
            2. 調(diào)用 module 對象的 addDependency 將依賴對象加入到 module 依賴列表中
          5. AST 遍歷完畢后,調(diào)用 module.handleParseResult 處理模塊依賴
          6. 對于 module 新增的依賴,調(diào)用 handleModuleCreate ,控制流回到第一步
          7. 所有依賴都解析完畢后,構(gòu)建階段結(jié)束

          這個過程中數(shù)據(jù)流 module => ast => dependences => module ,先轉(zhuǎn) AST 再從 AST 找依賴。這就要求 loaders 處理完的最后結(jié)果必須是可以被 acorn 處理的標(biāo)準(zhǔn) JavaScript 語法,比如說對于圖片,需要從圖像二進(jìn)制轉(zhuǎn)換成類似于 export default "data:image/png;base64,xxx" 這類 base64 格式或者 export default "http://xxx" 這類 url 格式。

          compilation 按這個流程遞歸處理,逐步解析出每個模塊的內(nèi)容以及 module 依賴關(guān)系,后續(xù)就可以根據(jù)這些內(nèi)容打包輸出。

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

          假如有如下圖所示的文件依賴樹:

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

          此時得到 module[index.js] 的內(nèi)容以及對應(yīng)的依賴對象 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)沒有更多新的依賴,就可以繼續(xù)推進(jìn),進(jìn)入下一步。

          總結(jié)

          回顧章節(jié)開始時提到的問題:

          • Webpack 編譯過程會將源碼解析為 AST 嗎?webpack 與 babel 分別實(shí)現(xiàn)了什么?
            • 構(gòu)建階段會讀取源碼,解析為 AST 集合。
            • Webpack 讀出 AST 之后僅遍歷 AST 集合;babel 則對源碼做等價轉(zhuǎn)換
          • Webpack 編譯過程中,如何識別資源對其他資源的依賴?
            • Webpack 遍歷 AST 集合過程中,識別 require/ import 之類的導(dǎo)入語句,確定模塊對其他資源的依賴關(guān)系
          • 相對于 grant、gulp 等流式構(gòu)建工具,為什么 webpack 會被認(rèn)為是新一代的構(gòu)建工具?
            • Grant、Gulp 僅執(zhí)行開發(fā)者預(yù)定義的任務(wù)流;而 webpack 則深入處理資源的內(nèi)容,功能上更強(qiáng)大

          生成階段

          基本流程

          構(gòu)建階段圍繞 module 展開,生成階段則圍繞 chunks 展開。經(jīng)過構(gòu)建階段之后,webpack 得到足夠的模塊內(nèi)容與模塊關(guān)系信息,接下來開始生成最終資源了。代碼層面,就是開始執(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 原意密封、上鎖,我個人理解在 webpack 語境下接近于 “將模塊裝進(jìn)蜜罐”seal 函數(shù)主要完成從 modulechunks 的轉(zhuǎn)化,核心流程:

          簡單梳理一下:

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

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

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

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

          示例:多入口打包

          假如有這樣的配置:

          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: [],
          };

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

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

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

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

          也就是根據(jù)依賴關(guān)系,chunk[a] 包含了 index-a/c 兩個模塊;chunk[b] 包含了 c/index-b/d 三個模塊;chunk[e-hash] 為動態(tài)引入 e 對應(yīng)的 chunk。

          不知道大家注意到?jīng)]有,chunk[a]chunk[b] 同時包含了 c,這個問題放到具體業(yè)務(wù)場景可能就是,一個多頁面應(yīng)用,所有頁面都依賴于相同的基礎(chǔ)庫,那么這些所有頁面對應(yīng)的 entry 都會包含有基礎(chǔ)庫代碼,這豈不浪費(fèi)?為了解決這個問題,webpack 提供了一些插件如 CommonsChunkPluginSplitChunksPlugin,在基本規(guī)則之外進(jìn)一步優(yōu)化 chunks 結(jié)構(gòu)。

          SplitChunksPlugin 的作用

          SplitChunksPlugin 是 webpack 架構(gòu)高擴(kuò)展的一個絕好的示例,我們上面說了 webpack 主流程里面是按 entry / 動態(tài)引入 兩種情況組織 chunks 的,這必然會引發(fā)一些不必要的重復(fù)打包,webpack 通過插件的形式解決這個問題。

          回顧 compilation.seal 函數(shù)的代碼,大致上可以梳理成這么4個步驟:

          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個步驟觸發(fā)的 optimizeChunks 鉤子,這個時候已經(jīng)跑完主流程的邏輯,得到 chunks 集合,SplitChunksPlugin 正是使用這個鉤子,分析 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ò)展性,使得整個編譯的主流程是可以固化下來的,分支邏輯和細(xì)節(jié)需求“外包”出去由第三方實(shí)現(xiàn),這套規(guī)則架設(shè)起了龐大的 webpack 生態(tài),關(guān)于插件架構(gòu)的更多細(xì)節(jié),下面 plugin 部分有詳細(xì)介紹,這里先跳過。

          寫入文件系統(tǒng)

          經(jīng)過構(gòu)建階段后,compilation 會獲知資源模塊的內(nèi)容與依賴關(guān)系,也就知道“輸入”是什么;而經(jīng)過 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)邏輯比較曲折,但是與主流程沒有太多關(guān)系,所以這里就不展開講了。

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

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

          • compiler.make 階段:
            • entry 文件以 dependence 對象形式加入 compilation 的依賴列表,dependence 對象記錄有 entry 的類型、路徑等信息
            • 根據(jù) dependence 調(diào)用對應(yīng)的工廠函數(shù)創(chuàng)建 module 對象,之后讀入 module 對應(yīng)的文件內(nèi)容,調(diào)用 loader-runner 對內(nèi)容做轉(zhuǎn)化,轉(zhuǎn)化結(jié)果若有其它依賴則繼續(xù)讀入依賴資源,重復(fù)此過程直到所有依賴均被轉(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ā)布器只是在特定時機(jī)發(fā)布事件消息,訂閱者并不或者很少與事件直接發(fā)生交互,舉例來說,我們平常在使用 HTML 事件的時候很多時候只是在這個時機(jī)觸發(fā)業(yè)務(wù)邏輯,很少調(diào)用上下文操作。而 webpack 的鉤子體系是一種強(qiáng)耦合架構(gòu),它在特定時機(jī)觸發(fā)鉤子時會附帶上足夠的上下文信息,插件定義的鉤子回調(diào)中,能也只能與這些上下文背后的數(shù)據(jù)結(jié)構(gòu)、接口交互產(chǎn)生 side effect,進(jìn)而影響到編譯狀態(tài)和后續(xù)流程。

          學(xué)習(xí)插件架構(gòu),需要理解三個關(guān)鍵問題:

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

          What: 什么是插件

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

          class SomePlugin {
              apply(compiler) {
              }
          }

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

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

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

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

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

          When: 什么時候會觸發(fā)鉤子

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

          • compiler.hooks.compilation
            • 時機(jī):啟動編譯創(chuàng)建出 compilation 對象后觸發(fā)
            • 參數(shù):當(dāng)前編譯的 compilation 對象
            • 示例:很多插件基于此事件獲取 compilation 實(shí)例
          • compiler.hooks.make
            • 時機(jī):正式開始編譯時觸發(fā)
            • 參數(shù):同樣是當(dāng)前編譯的 compilation 對象
            • 示例:webpack 內(nèi)置的 EntryPlugin 基于此鉤子實(shí)現(xiàn) entry 模塊的初始化
          • compilation.hooks.optimizeChunks
            • 時機(jī):seal 函數(shù)中,chunk 集合構(gòu)建完畢后觸發(fā)
            • 參數(shù):chunks 集合與 chunkGroups 集合
            • 示例:SplitChunksPlugin 插件基于此鉤子實(shí)現(xiàn) chunk 拆分優(yōu)化
          • compiler.hooks.done
            • 時機(jī):編譯完成后觸發(fā)
            • 參數(shù):stats 對象,包含編譯過程中的各類統(tǒng)計信息
            • 示例:webpack-bundle-analyzer 插件基于此鉤子實(shí)現(xiàn)打包分析

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

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

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

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

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

          參數(shù)

          傳遞參數(shù)與具體的鉤子強(qiáng)相關(guān),官網(wǎng)對這方面沒有做出進(jìn)一步解釋,我的做法是直接在源碼里面搜索調(diào)用語句,例如對于 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é)合代碼所在的上下文,可以判斷出此時傳遞的是經(jīng)過優(yōu)化的 chunksmodules 集合。

          找到示例

          Webpack 的鉤子復(fù)雜程度不一,我認(rèn)為最好的學(xué)習(xí)方法還是帶著目的去查詢其他插件中如何使用這些鉤子。例如,在 compilation.seal 函數(shù)內(nèi)部有 optimizeModulesafterOptimizeModules 這一對看起來很對偶的鉤子,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);
            }
          });

          基本上可以猜測出,afterOptimizeModules 的設(shè)計初衷就是用于通知優(yōu)化行為的結(jié)束。

          apply 雖然是一個函數(shù),但是從設(shè)計上就只有輸入,webpack 不 care 輸出,所以在插件中只能通過調(diào)用類型實(shí)體的各種方法來或者更改實(shí)體的配置信息,變更編譯行為。例如:

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

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

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

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

          • compilation.dependencyFactories.set
          • compilation.addEntry

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

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

          Loader 介紹

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

          流程圖中, runLoaders 會調(diào)用用戶所配置的 loader 集合讀取、轉(zhuǎn)譯資源,此前的內(nèi)容可以千奇百怪,但轉(zhuǎn)譯之后理論上應(yīng)該輸出標(biāo)準(zhǔn) JavaScript 文本或者 AST 對象,webpack 才能繼續(xù)處理模塊依賴。

          理解了這個基本邏輯之后,loader 的職責(zé)就比較清晰了,不外乎是將內(nèi)容 A 轉(zhuǎn)化為內(nèi)容 B,但是在具體用法層面還挺多講究的,有 pitch、pre、post、inline 等概念用于應(yīng)對各種場景。

          為了幫助理解,這里補(bǔ)充一個示例:Webpack 案例 -- vue-loader 原理分析。

          附錄

          源碼閱讀技巧

          • **避重就輕:**挑軟柿子捏,比如初始化過程雖然繞,但是相對來說是概念最少、邏輯最清晰的,那從這里入手摸清整個工作過程,可以習(xí)得 webpack 的一些通用套路,例如鉤子的設(shè)計與作用、編碼規(guī)則、命名習(xí)慣、內(nèi)置插件的加載邏輯等,相當(dāng)于先入了個門
          • **學(xué)會調(diào)試:**多用 ndb 單點(diǎn)調(diào)試功能追蹤程序的運(yùn)行,雖然 node 的調(diào)試有很多種方法,但是我個人更推薦 ndb ,靈活、簡單,配合 debugger 語句是大殺器
          • **理解架構(gòu):**某種程度上可以將 webpack 架構(gòu)簡化為 compiler + compilation + plugins ,webpack 運(yùn)行過程中只會有一個 compiler ;而每次編譯 —— 包括調(diào)用 compiler.run 函數(shù)或者 watch = true 時文件發(fā)生變更,都會創(chuàng)建一個 compilation 對象。理解這三個核心對象的設(shè)計、職責(zé)、協(xié)作,差不多就能理解 webpack 的核心邏輯了
          • 抓大放小: plugin 的關(guān)鍵是“鉤子”,我建議戰(zhàn)略上重視,戰(zhàn)術(shù)上忽視!鉤子畢竟是 webpack 的關(guān)鍵概念,是整個插件機(jī)制的根基,學(xué)習(xí) webpack 根本不可能繞過鉤子,但是相應(yīng)的邏輯跳轉(zhuǎn)實(shí)在太繞太不直觀了,看代碼的時候一直揪著這個點(diǎn)的話,復(fù)雜性會劇增,我的經(jīng)驗(yàn)是:
            • 認(rèn)真看一下 tapable 倉庫的文檔,或者粗略看一下 tapable 的源碼,理解同步鉤子、異步鉤子、promise 鉤子、串行鉤子、并行鉤子等概念,對 tapable 提供的事件模型有一個較為精細(xì)的認(rèn)知,這叫戰(zhàn)略上重視
            • 遇到不懂的鉤子別慌,我的經(jīng)驗(yàn)我連這個類都不清楚干啥的,要去理解這些鉤子實(shí)在太難了,不如先略過鉤子本身的含義,去看那些插件用到了它,然后到插件哪里去加 debugger 語句單點(diǎn)調(diào)試,等你縷清后續(xù)邏輯的時候,大概率你也知道鉤子的含義了,這叫戰(zhàn)術(shù)上忽視
          • **保持好奇心:**學(xué)習(xí)過程保持旺盛的好奇心和韌性,善于 & 敢于提出問題,然后基于源碼和社區(qū)資料去總結(jié)出自己的答案,問題可能會很多,比如:
            • loader 為什么要設(shè)計 pre、pitch、post、inline?
            • compilation.seal 函數(shù)內(nèi)部設(shè)計了很多優(yōu)化型的鉤子,為什么需要區(qū)分的這么細(xì)?webpack 設(shè)計者對不同鉤子有什么預(yù)期?
            • 為什么需要那么多 module 子類?這些子類分別在什么時候被使用?

          ModuleModule 子類

          從上文可以看出,webpack 構(gòu)建階段的核心流程基本上都圍繞著 module 展開,相信接觸過、用過 Webpack 的讀者對 module 應(yīng)該已經(jīng)有一個感性認(rèn)知,但是實(shí)現(xiàn)上 module 的邏輯是非常復(fù)雜繁重的。

          [email protected] 為例,直接或間接繼承自 Module (webpack/lib/Module.js 文件) 的子類有54個:

          無法復(fù)制加載中的內(nèi)容

          要一個一個捋清楚這些類的作用實(shí)在太累了,我們需要抓住本質(zhì):module 的作用是什么?

          module 是 webpack 資源處理的基本單位,可以認(rèn)為 webpack 對資源的路徑解析、讀入、轉(zhuǎn)譯、分析、打包輸出,所有操作都是圍繞著 module 展開的。有很多文章會說 module = 文件, 其實(shí)這種說法并不準(zhǔn)確,比如子類 AsyncModuleRuntimeModule 就只是一段內(nèi)置的代碼,是一種資源而不能簡單等價于實(shí)際文件。

          Webpack 擴(kuò)展性很強(qiáng),包括模塊的處理邏輯上,比如說入口文件是一個普通的 js,此時首先創(chuàng)建 NormalModule 對象,在解析 AST 時發(fā)現(xiàn)這個文件里還包含了異步加載語句,例如 requere.ensure ,那么相應(yīng)地會創(chuàng)建 AsyncModuleRuntimeModule 模塊,注入異步加載的模板代碼。上面類圖的 54 個 module 子類都是為適配各種場景設(shè)計的。




          瀏覽 58
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  成人区人妻精品一 | 日韩无码三级视频 | 国产视频一区二区三区四区五区 | 大香蕉夜夜 | 无码区一区二区三区 |