有点难的 webpack 知识点:Dependency Graph 深度解析

299次阅读  |  发布于3年以前

背景

Dependency Graph 概念来自官网 Dependency Graph | webpack 一文,原文解释是这样的:

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.

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 处理应用代码时,会从开发者提供的 entry 开始递归地组建起包含所有模块的 dependency graph _,_之后再将这些 module 打包为 bundles 。

然而事实远不止官网描述的这么简单,Dependency Graph 贯穿 webpack 整个运行周期,从 make 阶段的模块解析,到 seal 阶段的 chunk 生成,以及 tree-shaking 功能都高度依赖于Dependency Graph ,是 webpack 资源构建的一个非常核心的数据结构。

本文将围绕 webpack@v5.x 的 Dependency Graph 实现,展开讨论三个方面的内容:

学习本文,您将进一步了解 webpack 模块解析的处理细节,结合前文 [万字总结] 一文吃透 Webpack 核心原理 ,您可以更透彻地了解 webpack 的核心机制。

Dependency Graph

本节将深入 webpack 源码,解读 Dependency Graph 的内在数据结构及依赖关系收集过程。在正式展开之前,有必要回顾几个 webpack 重要的概念:

数据结构

Webpack 4.x 的 Dependency Graph 实现较简单,主要由 Dependence/Module 内置的系列属性记录引用、被引用关系。

而 Webpack 5.0 之后则实现了一套相对复杂的类结构记录模块间依赖关系,将模块依赖相关的逻辑从 Dependence/Module 解耦为一套独立的类型结构,主要类型有:

类间关系大致为:

上面类图需要额外注意:

依赖收集过程

ModuleGraphModuleGraphConnectionModuleGraphModule 三者协作,在 webpack 构建过程(make 阶段)中逐步收集模块间的依赖关系,回顾前文 [万字总结] 一文吃透 Webpack 核心原理 提及的构建流程图:

构建流程本身很复杂,建议读者对比阅读 [万字总结] 一文吃透 Webpack 核心原理 一文,加深理解。依赖关系收集过程主要发生在两个节点:

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 信息查询的工具函数,例如:

等等。

Webpack@v5.x 内部的许多插件、Dependency 子类、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 都会有极大的帮助。在分析过程其实也挖掘出了很多新的知识盲点:

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8