Webpack 系列第三篇:Dependency Graph 深度解析

全文 2500 字,閱讀時長約 30 分鐘。若是以爲文章有用,歡迎點贊關注,但寫做實屬不易,未經做者贊成,禁止任何形式轉載!!!

背景

Dependency Graph 概念來自官網 Dependency Graph | webpack 一文,原文解釋是這樣的:javascript

Any time one file depends on another, webpack treats this as a dependency_. This allows webpack to take non-code assets, such as images or web fonts, and also provide them as _dependencies for your application.java

When webpack processes your application, it starts from a list of modules defined on the command line or in its configuration file. Starting from these entry points_, webpack recursively builds a _dependency graph that includes every module your application needs, then bundles all of those modules into a small number of bundles - often, just one - to be loaded by the browser.webpack

翻譯過來核心意思是:webpack 處理應用代碼時,會從開發者提供的 entry 開始遞歸地組建起包含全部模塊的 dependency graph _,_以後再將這些 module 打包爲 bundles 。web

然而事實遠不止官網描述的這麼簡單,Dependency Graph 貫穿 webpack 整個運行週期,從 make 階段的模塊解析,到 seal 階段的 chunk 生成,以及 tree-shaking 功能都高度依賴於Dependency Graph ,是 webpack 資源構建的一個很是核心的數據結構。算法

本文將圍繞 webpack\@v5.x 的 Dependency Graph 實現,展開討論三個方面的內容:數據結構

  • Dependency Graph 在 webpack 實現中以何種數據結構呈現
  • Webpack 運行過程當中如何收集模塊間依賴關係,進而構建出 Dependency Graph
  • Dependency Graph 構建完畢後,又是如何被消費的

學習本文,您將進一步瞭解 webpack 模塊解析的處理細節,結合前文 [萬字總結] 一文吃透 Webpack 核心原理 ,您能夠更透徹地瞭解 webpack 的核心機制。架構

image.png

關注公衆號【Tecvan】,回覆【1】,獲取 Webpack 知識體系腦圖

Dependency Graph

本節將深刻 webpack 源碼,解讀 Dependency Graph 的內在數據結構及依賴關係收集過程。在正式展開以前,有必要回顧幾個 webpack 重要的概念:app

  • Module:資源在 webpack 內部的映射對象,包含了資源的路徑、上下文、依賴、內容等信息
  • Dependency :在模塊中引用其它模塊,例如 import "a.js" 語句,webpack 會先將引用關係表述爲 Dependency 子類並關聯 module 對象,等到當前 module 內容都解析完畢以後,啓動下次循環開始將 Dependency 對象轉換爲適當的 Module 子類。
  • Chunk :用於組織輸出結構的對象,webpack 分析完全部模塊資源的內容,構建出完整的 Dependency Graph 以後,會根據用戶配置及 Dependency Graph 內容構建出一個或多個 chunk 實例,每一個 chunk 與最終輸出的文件大體上是一一對應的。

數據結構

Webpack 4.x 的 Dependency Graph 實現較簡單,主要由 Dependence/Module 內置的系列屬性記錄引用、被引用關係。ide

而 Webpack 5.0 以後則實現了一套相對複雜的類結構記錄模塊間依賴關係,將模塊依賴相關的邏輯從 Dependence/Module 解耦爲一套獨立的類型結構,主要類型有:函數

  • ModuleGraph :記錄 Dependency Graph 信息的容器,一方面保存了構建過程當中涉及到的全部 moduledependency 對象,以及這些對象互相之間的引用;另外一方面提供了各類工具方法,方便使用者迅速讀取出 moduledependency 附加的信息
  • ModuleGraphConnection :記錄模塊間引用關係的數據結構,內部經過 originModule 屬性記錄引用關係中的父模塊,經過 module 屬性記錄子模塊。此外還提供了一系列函數工具用於判斷對應的引用關係的有效性
  • ModuleGraphModuleModule 對象在 Dependency Graph 體系下的補充信息,包含模塊對象的 incomingConnections —— 指向模塊自己的 ModuleGraphConnection 集合,即誰引用了模塊本身;outgoingConnections —— 該模塊對外的依賴,即該模塊引用了其餘那些模塊。

類間關係大體爲:

上面類圖須要額外注意:

  • ModuleGraph 對象經過 _dependencyMap 屬性記錄 Dependency 對象與 ModuleGraphConnection 鏈接對象之間的映射關係,後續的處理中能夠基於這層映射迅速找到 Dependency 實例對應的引用與被引用者
  • ModuleGraph 對象經過 _moduleMapmodule 基礎上附加 ModuleGraphModule 信息,而 ModuleGraphModule 最大的做用就是記錄了模塊的引用與被引用關係,後續的處理能夠基於該屬性找到 module 實例的全部依賴與被依賴關係

依賴收集過程

ModuleGraphModuleGraphConnectionModuleGraphModule 三者協做,在 webpack 構建過程(make 階段)中逐步收集模塊間的依賴關係,回顧前文 [萬字總結] 一文吃透 Webpack 核心原理 說起的構建流程圖:

構建流程自己很複雜,建議讀者對比閱讀 [萬字總結] 一文吃透 Webpack 核心原理 一文,加深理解。依賴關係收集過程主要發生在兩個節點:

  • addDependency :webpack 從模塊內容中解析出引用關係後,建立適當的 Dependency 子類並調用該方法記錄到 module 實例
  • handleModuleCreation :模塊解析完畢後,webpack 遍歷父模塊的依賴集合,調用該方法建立 Dependency 對應的子模塊對象,以後調用 compilation.moduleGraph.setResolvedModule 方法將父子引用信息記錄到 moduleGraph 對象上

setResolvedModule 方法的邏輯大體爲:

class ModuleGraph {
    constructor() {
        /** @type {Map<Dependency, ModuleGraphConnection>} */
        this._dependencyMap = new Map();
        /** @type {Map<Module, ModuleGraphModule>} */
        this._moduleMap = new Map();
    }

    /**
     * @param {Module} originModule the referencing module
     * @param {Dependency} dependency the referencing dependency
     * @param {Module} module the referenced module
     * @returns {void}
     */
    setResolvedModule(originModule, dependency, module) {
        const connection = new ModuleGraphConnection(
            originModule,
            dependency,
            module,
            undefined,
            dependency.weak,
            dependency.getCondition(this)
        );
        this._dependencyMap.set(dependency, connection);
        const connections = this._getModuleGraphModule(module).incomingConnections;
        connections.add(connection);
        const mgm = this._getModuleGraphModule(originModule);
        if (mgm.outgoingConnections === undefined) {
            mgm.outgoingConnections = new Set();
        }
        mgm.outgoingConnections.add(connection);
    }
}

上例代碼主要更改了 _dependencyMapmoduleGraphModule 的出入 connections 屬性,以此收集當前模塊的上下游依賴關係。

實例解析

看個簡單例子,對於下面的依賴關係:

Webpack 啓動後,在構建階段遞歸調用 compilation.handleModuleCreation 函數,逐步補齊 Dependency Graph 結構,最終可能生成以下數據結果:

ModuleGraph: {
    _dependencyMap: Map(3){
        { 
            EntryDependency{request: "./src/index.js"} => ModuleGraphConnection{
                module: NormalModule{request: "./src/index.js"}, 
                // 入口模塊沒有引用者,故設置爲 null
                originModule: null
            } 
        },
        { 
            HarmonyImportSideEffectDependency{request: "./src/a.js"} => ModuleGraphConnection{
                module: NormalModule{request: "./src/a.js"}, 
                originModule: NormalModule{request: "./src/index.js"}
            } 
        },
        { 
            HarmonyImportSideEffectDependency{request: "./src/a.js"} => ModuleGraphConnection{
                module: NormalModule{request: "./src/b.js"}, 
                originModule: NormalModule{request: "./src/index.js"}
            } 
        }
    },

    _moduleMap: Map(3){
        NormalModule{request: "./src/index.js"} => ModuleGraphModule{
            incomingConnections: Set(1) [
                // entry 模塊,對應 originModule 爲null
                ModuleGraphConnection{ module: NormalModule{request: "./src/index.js"}, originModule:null }
            ],
            outgoingConnections: Set(2) [
                // 從 index 指向 a 模塊
                ModuleGraphConnection{ module: NormalModule{request: "./src/a.js"}, originModule: NormalModule{request: "./src/index.js"} },
                // 從 index 指向 b 模塊
                ModuleGraphConnection{ module: NormalModule{request: "./src/b.js"}, originModule: NormalModule{request: "./src/index.js"} }
            ]
        },
        NormalModule{request: "./src/a.js"} => ModuleGraphModule{
            incomingConnections: Set(1) [
                ModuleGraphConnection{ module: NormalModule{request: "./src/a.js"}, originModule: NormalModule{request: "./src/index.js"} }
            ],
            // a 模塊沒有其餘依賴,故 outgoingConnections 屬性值爲 undefined
            outgoingConnections: undefined
        },
        NormalModule{request: "./src/b.js"} => ModuleGraphModule{
            incomingConnections: Set(1) [
                ModuleGraphConnection{ module: NormalModule{request: "./src/b.js"}, originModule: NormalModule{request: "./src/index.js"} }
            ],
            // b 模塊沒有其餘依賴,故 outgoingConnections 屬性值爲 undefined
            outgoingConnections: undefined
        }
    }
}

從上面的 Dependency Graph 能夠看出,本質上 ModuleGraph._moduleMap 已經造成了一個有向無環圖結構,其中字典 _moduleMap 的 key 爲圖的節點,對應 value ModuleGraphModule 結構中的 outgoingConnections 屬性爲圖的邊,則上例中從起點 index.js 出發沿 outgoingConnections 向前可遍歷出圖的全部頂點。

做用

以 webpack\@v5.16.0 爲例,關鍵字 moduleGraph 出現了 1277 次,幾乎覆蓋了 webpack/lib 文件夾下的全部文件,其做用可見一斑。雖然出現的頻率很高,但總的來講能夠看出有兩個主要做用:信息索引、轉變爲 ChunkGraph 以肯定輸出結構。

信息索引

ModuleGraph 類型提供了不少實現 module / dependency 信息查詢的工具函數,例如:

  • getModule(dep: Dependency) :根據 dep 查找對應的 module 實例
  • getOutgoingConnections(module: Module) :查找 module 實例的全部依賴
  • getIssuer(module: Module) :查找 module 在何處被引用(關於 issuer 機制的更多信息,可參考個人另外一篇文章: 十分鐘精進 Webpack:module.issuer 屬性詳解 )

等等。

Webpack\@v5.x 內部的許多插件、Dependency 子類、Module 子類的實現都須要用到這些工具函數查找特定模塊、依賴的信息,例如:

  • SplitChunksPlugin 在優化 chunks 處理中,須要使用 moduleGraph.getExportsInfo 查詢各個模塊的 exportsInfo (模塊導出的信息集合,與 tree-shaking 強相關,後續會單出一篇文章講解)信息以肯定如何分離 chunk
  • compilation.seal 函數中,須要遍歷 entry 對應的 dep 並調用 moduleGraph.getModule 獲取完整的 module 定義
  • ...

那麼,在您編寫插件時,能夠考慮適度參考 webpack/lib/ModuleGraph.js 中提供的方法,確承認以獲取使用那些函數獲取到您所須要的信息。

構建 ChunkGraph

Webpack 主體流程中,make 構建階段結束以後會進入 seal 階段,開始梳理以何種方式組織輸出內容。在 webpack\@v4.x 時,seal 階段主要圍繞 ChunkChunkGroup 兩個類型展開,而到了 5.0 以後,與 Dependency Graph 相似也引入了一套全新的基於 ChunkGraph 的圖結構實現資源生成算法。

在 compilation.seal 函數中,首先根據默認規則 —— 每一個 entry 對應組織爲一個 chunk ,以後調用 webpack/lib/buildChunkGraph.js 文件定義的 buildChunkGraph 方法,遍歷 make 階段生成的 moduleGraph 對象從而將 module 依賴關係轉化爲 chunkGraph 對象。

這一塊的邏輯也特別複雜,不在這裏展開,下次會單獨出一篇文章講解 chunk/chunkGroup/chunkGraph 等對象構築成的模塊輸出規則。

總結

本文討論的 Dependency Graph 概念在 webpack 內部被大量使用,所以理解這個概念對咱們理解 webpack 源碼,或者學習如何編寫插件、loader 都會有極大的幫助。在分析過程其實也挖掘出了不少新的知識盲點:

  • Chunk 的完整機制是怎麼樣的?
  • Dependency 的完總體系是如何實現的,有何做用?
  • Module 的 exportsInfo 如何收集?在 tree-shaking 過程當中如何被使用?

若是你也對上述問題感興趣,歡迎點贊關注,後續會圍繞 webpack 輸出更多有用的文章。

image.png

往期文章:

相關文章
相關標籤/搜索