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

          [全文高能] 一文吃透 Webpack 核心原理

          共 21484字,需瀏覽 43分鐘

           ·

          2021-05-16 03:07

          背景

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

          時(shí)至 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 個(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ì)展開太多 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):

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

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

          單次構(gòu)建過(guò)程自上而下按順序執(zhí)行,下面會(huì)展開聊聊細(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)聽這些事件,在特定時(shí)間點(diǎn)介入編譯過(guò)程

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

          初始化階段

          基本流程

          學(xué)習(xí)一個(gè)項(xiàng)目的源碼通常都是從入口開始看起,按圖索驥慢慢摸索出套路的,所以先來(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è)好了,緊接著開始調(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.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ò)一坨與主流程無(wú)關(guān)的 hook 之后,再調(diào)用 handleModuleCreate 函數(shù),正式開始構(gòu)建內(nèi)容

          這個(gè)過(guò)程需要在 webpack 初始化的時(shí)候預(yù)埋下各種插件,經(jīng)歷 4 個(gè)文件,7次跳轉(zhuǎn)才開始進(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 開始遞歸解析資源與資源的依賴,在 compilation 對(duì)象內(nèi)逐步構(gòu)建出 module 集合以及 module 之間的依賴關(guān)系,核心流程:

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

          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ā)各種鉤子
            1. HarmonyExportDependencyParserPlugin 插件監(jiān)聽 exportImportSpecifier 鉤子,解讀 JS 文本對(duì)應(yīng)的資源依賴
            2. 調(diào)用 module 對(duì)象的 addDependency 將依賴對(duì)象加入到 module 依賴列表中
          5. AST 遍歷完畢后,調(diào)用 module.handleParseResult 處理模塊依賴
          6. 對(duì)于 module 新增的依賴,調(diào)用 handleModuleCreate ,控制流回到第一步
          7. 所有依賴都解析完畢后,構(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)

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

          其中 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)部會(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é)開始時(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í)行開發(fā)者預(yù)定義的任務(wù)流;而 webpack 則深入處理資源的內(nèi)容,功能上更強(qiáng)大

          生成階段

          基本流程

          構(gòu)建階段圍繞 module 展開,生成階段則圍繞 chunks 展開。經(jīng)過(guò)構(gòu)建階段之后,webpack 得到足夠的模塊內(nèi)容與模塊關(guān)系信息,接下來(lá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ù)主要完成從 modulechunks 的轉(zhuǎn)化,核心流程:

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

          1. 構(gòu)建本次編譯的 ChunkGraph 對(duì)象;
          2. 遍歷 compilation.modules 集合,將 moduleentry/動(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"),
            },
            devtool: false,
            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 提供了一些插件如 CommonsChunkPluginSplitChunksPlugin,在基本規(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)系,所以這里就不展開講了。

          資源形態(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)建而成,插件開發(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ì)略有不同,插件開發(fā)者需要根據(jù)這些的特性,編寫不同的交互邏輯,這部分內(nè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)有太大收獲,所以有必要展開聊一下這個(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ī):正式開始編譯時(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)化的 chunksmodules 集合。

          找到示例

          Webpack 的鉤子復(fù)雜程度不一,我認(rèn)為最好的學(xué)習(xí)方法還是帶著目的去查詢其他插件中如何使用這些鉤子。例如,在 compilation.seal 函數(shù)內(nèi)部有 optimizeModulesafterOptimizeModules 這一對(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 的插件體系與平常所見的 訂閱/發(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í)候被使用?

          ModuleModule 子類

          從上文可以看出,webpack 構(gòu)建階段的核心流程基本上都圍繞著 module 展開,相信接觸過(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è):

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

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

          module 是 webpack 資源處理的基本單位,可以認(rèn)為 webpack 對(duì)資源的路徑解析、讀入、轉(zhuǎn)譯、分析、打包輸出,所有操作都是圍繞著 module 展開的。有很多文章會(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ì)的。

          瀏覽 45
          點(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>
                  午夜精品久久久久久久 | 特黄av毛片 | 91大神在线看 | 国产精品嫩草AV城中村 | 大逼视频偷怕 |