Webpack: Dependency Graph 管理模块间依赖

news2024/10/7 6:41:50

概述

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 整个运行周期,从「构建阶段」的模块解析,到「生成阶段」的 Chunk 生成,以及 Tree-shaking 等功能都高度依赖于Dependency Graph ,是 Webpack 资源构建流程中一个非常核心的数据结构。

本文将围绕 Webpack5 的 Dependency Graph 实现,展开讨论如下内容:

  • 模块依赖关系图的概念;
  • Webpack5 如何收集、管理、消费模块依赖关系图。

Dependency Graph 是什么?

正式介绍 Dependency Graph 结构之前,我们先 简单 回顾一下 Webpack 构建阶段的关键过程:
请添加图片描述

  1. 首先根据 entry 配置信息创建若干 EntryDependency 对象;
  2. 调用 NormalModuleFactory ,根据 EntryDependency 对象的资源路径创建 Module 子类对象;
  3. Module 代码解析为 AST 结构;
  4. 遍历 AST,找到所有模块导入语句(import/require);
  5. 根据导入语句创建对应的 Dependency 子类对象;
  6. 递归执行步骤 2,直到所有项目文件都处理完毕。

这个过程从 entry 模块开始,逐步递归找出所有依赖文件,模块之间隐式形成了以 entry 为起点,以模块为节点,以导入导出依赖为边的有向图关系 —— 也就是 Webpack 官网所说的 Dependency Graph。
请添加图片描述

这个 Dependency Graph 是 Webpack 内部非常重要的过程信息之一,后续封装 Chunk、Code Splits、Tree-Shaking、Hot Module Replacement 等等,几乎所有功能都需要依赖这一信息实现。

Webpack5 之前,Dependency Graph 关系隐含在 Dependence、Module 对象的一系列属性中,例如:

  • 通过 module.dependencies 数组记录模块依赖对象;
  • 通过 dependency.module 记录依赖对应的模块对象引用;
  • 通过 module.issuer 记录父模块引用。

这种设计存在很多问题,例如:模块之间的关系非常隐晦难懂;模块搜索算法与模块资源构建逻辑耦合在同一个 Class 实体内,Module 职责不单一且复杂度非常高;同一个 Module 对象无法在多个 Dependency Graph 之间共享,等等。

为此,Webpack5 重构 了 Dependency Graph 的具体实现,将依赖关系从 Dependence/Module 类型中解耦出来,以一套独立的 Graph 数据结构记录模块间依赖关系,并基于 Map/Set 等原生模块实现更高效的模块搜索、校验、遍历算法。

Dependency Graph 数据结构详解

Webpack 5.0 之后的 Dependency Graph 涉及如下数据类型:

  • ModuleGraph:记录 Dependency Graph 信息的容器,记录构建过程中涉及到的所有 moduledependency 对象,以及这些对象互相之间的引用;
  • ModuleGraphConnection :记录模块间引用关系的数据结构,内部通过 originModule 属性记录引用关系中的父模块,通过 module 属性记录子模块;
  • ModuleGraphModuleModule 对象在 Dependency Graph 体系下的补充信息,包含模块对象的 incomingConnections —— 指向模块本身的 ModuleGraphConnection 集合,即谁引用了模块自身;outgoingConnections —— 该模块对外的依赖,即该模块引用了其他那些模块。

在这里插入图片描述

这些类型之间关系的基本逻辑是:

  • Compilation 类内部会维护一个全局唯一的 ModuleGraph 实例对象;
  • 每次解析出新模块后,将 Module、Dependency,以及模块之间的关系 —— ModuleConnection 记录到 compilation.moduleGraph 对象中;
  • ModuleGraph 除了记录依赖关系外,还提供了许多工具方法,方便使用者迅速读取出
    • moduledependency 附加的信息
  • ModuleGraph 内部有两个关键属性:
    • 通过 _dependencyMap 属性记录 Dependency 对象与 ModuleGraphConnection 连接对象之间的映射关系,后续的处理中可以基于这层映射迅速找到 Dependency 实例对应的引用与被引用者;
    • 通过 _moduleMap 属性记录 ModuleModuleGraphModule 之间的映射关系。

最终,通过 ModuleGraphModuleGraphConnectionModuleGraphModule 三种类型的协作,在主体的 ModuleDependency 体系之外,记录模块之间的依赖信息。

依赖关系收集过程,主要发生在构建阶段的两个节点:
请添加图片描述

  • addDependency :webpack 从模块内容中解析出引用关系后,创建适当的 Dependency 子类并调用该方法记录到 module 实例;
  • handleModuleCreation :模块解析完毕后,webpack 遍历父模块的依赖集合,调用该方法创建 Dependency 对应的子模块对象,之后调用 moduleGraph.setResolvedModule 方法将父子引用信息记录到 moduleGraph 对象上。

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 启动后:

  1. 首先创建 index.js 对应的 EntryDependency 对象;
  2. 调用 NormalModuleFactory 创建 EntryDependency 对应的 NormalModule 实例;
  3. 执行 compilation.handleModuleCreation,经过解析、遍历 AST 等操作,最终得到 a.jsb.js 两个新 Dependency 对象;
  4. 调用 NormalModuleFactory 为这两个 Dependency 对象创建对应的 NormalModule 对象;
  5. 调用 moduleGraph.setResolvedModule 记录 entry 模块与 a/b 模块的依赖关系。

最终生成如下数据结果:

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 向前可遍历出图的所有顶点。

作用

Webpack5 中,关键字 moduleGraph 出现了 1000 多次,几乎覆盖了 webpack/lib 文件夹下的所有文件,其作用可见一斑。虽然出现的频率很高,但总的来说可以看出有两个主要作用:信息索引,以及辅助构建 ChunkGraph。

信息索引是 ModuleGraph 最重要的功能,在 ModuleGraph 类型中提供了很多实现 module / dependency 信息查询的工具函数,例如:

  • getModule(dep: Dependency) :根据 dep 查找对应的 module 实例;
  • getOutgoingConnections(module) :查找 module 实例的所有依赖;
  • getIssuer(module: Module) :查找 module 在何处被引用;
  • 等等。

Webpack5 内部的许多插件、Dependency 子类、Module 子类的实现都需要用到这些工具函数查找特定模块、依赖的信息,例如:

  • SplitChunksPlugin 在优化 chunks 处理中,需要使用 moduleGraph.getExportsInfo 查询各个模块的 exportsInfo (模块导出的信息集合,与 tree-shaking 强相关,后续会单出一篇文章讲解)信息以确定如何分离 chunk。
  • compilation.seal 函数中,需要遍历 entry 对应的 dep 并调用 moduleGraph.getModule 获取完整的 module 定义

所以,你在编写插件时,可以考虑适度参考 webpack/lib/ModuleGraph.js 中提供的方法,确认可以获取使用哪些函数,获取到你所需要的信息。

此外,在 Webpack 完成模块构建,进入「生成阶段」之后,会按一系列规则将模块逐一分配到不同 Chunk 对象中,在 Webpack4 时,这个过程主要围绕 ChunkChunkGroup 两个类型展开。

而 5.0 之后,对 Chunk 之间的依赖关系管理也做了一次大型 重构:首先根据默认规则为每一个 entry 创建对应 Chunk 对象 ,之后调用 buildChunkGraph 方法,遍历 moduleGraph 对象,找出入口模块对应的所有 Module 对象,并将依赖关系转化为 ChunkGraph 对象,这一块的逻辑也特别复杂,我们放在下一章节讲解。

总结

综上,Webpack 构建过程中会持续收集模块之间的引用、被引用关系,并记录到 Dependency Graph 结构中,后续的 Chunk 封装、Code Split、Tree-Shaking 等,但凡需要分析模块关系的功能都强依赖于 Dependency Graph。

可以说,Dependency Graph 是 Webpack 底层最关键的模块地图数据,因此在 Webpack5 之后,Dependency Graph 结构被解耦抽离为以 ModuleGraph 为中心的若干独立类型,架构逻辑更合理,模块搜索、分析效率也得到不同程度优化,进而使得 Webpack5 构建速度也有明显提升。

学习 Dependency Graph,一是能帮助我们从数据结构角度,更深入理解 Webpack 模块读入与分析处理的过程;二是编写自定义插件时,可以通过 ModuleGraph 提供的若干工具函数了解模块之间的相互依赖关系。

思考 Dependency Graph 在 Webpack 的「构建阶段」与「生成阶段」分别扮演什么样的角色?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1890413.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

算法day1 两数之和 两数相加 冒泡排序 快速排序

两数之和 最简单的思维方式肯定是去凑两个数&#xff0c;两个数的和是目标值就ok。这里两遍for循环解决。 两数相加 敲了一晚上哈哈&#xff0c;结果超过int范围捏&#xff0c;难受捏。 public class Test2 {public static void main(String[] args) { // ListNode l1 …

像学Excel 一样学 Pandas系列-创建数据分析维度

嗨&#xff0c;小伙伴们。又到喜闻乐见的Python 数据分析王牌库 Pandas 的学习时间。按照数据分析处理过程&#xff0c;这次轮到了新增维度的部分了。 老样子&#xff0c;我们先来回忆一下&#xff0c;一个完整数据分析的过程&#xff0c;包含哪些部分内容。 其中&#xff0c…

四十篇:内存巨擘对决:Redis与Memcached的深度剖析与多维对比

内存巨擘对决&#xff1a;Redis与Memcached的深度剖析与多维对比 1. 引言 在现代的系统架构中&#xff0c;内存数据库已经成为了信息处理的核心技术之一。这类数据库系统的高效性主要来源于其对数据的即时访问能力&#xff0c;这是因为数据直接存储在RAM中&#xff0c;而非传统…

二叉树的前中后序遍历(递归法、迭代法)leetcode144、94/145

leetcode144、二叉树的前序遍历 给你二叉树的根节点 root &#xff0c;返回它节点值的 前序 遍历。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,2,3] 示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;[] 示例 3&#xff1a; 输…

前端入门超级攻略:你的第一步学习指南

如果您觉得这篇文章有帮助的话&#xff01;给个点赞和评论支持下吧&#xff0c;感谢~ 作者&#xff1a;前端小王hs 阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主/知名前端开发者/网络工程师 前言 由于前端技术的快速迭代性&#xff0c;国…

解决ps暂存盘已满的问题

点击编辑->首选项->暂存盘 ps默认暂存盘使用的是c盘&#xff0c;我们改成d盘即可 然后重启ps

STM32之五:TIM定时器(2-通用定时器)

目录 通用定时器&#xff08;TIM2~5&#xff09;框图 1、 输入时钟源选择 2、 时基单元 3 、输入捕获&#xff1a;&#xff08;IC—Input Capture&#xff09; 3.1 输入捕获通道框图&#xff08;TI1为例&#xff09; 3.1.1 滤波器&#xff1a; 3.1.2 边沿检测器&#xf…

移动智能终端数据安全管理方案

随着信息技术的飞速发展&#xff0c;移动设备已成为企业日常运营不可或缺的工具。特别是随着智能手机和平板电脑等移动设备的普及&#xff0c;这些设备存储了大量的个人和敏感数据&#xff0c;如银行信息、电子邮件等。员工通过智能手机和平板电脑访问企业资源&#xff0c;提高…

【等保2.0是什么意思?等保2.0的基本要求有哪些? 】

一、等保2.0是什么意思&#xff1f; 等保2.0又称“网络安全等级保护2.0”体系&#xff0c;它是国家的一项基本国策和基本制度。在1.0版本的基础上&#xff0c;等级保护标准以主动防御为重点&#xff0c;由被动防守转向安全可信&#xff0c;动态感知&#xff0c;以及事前、事中…

SSM玉林师范学院宿舍管理系统-计算机毕业设计源码19633

摘要 随着大学生人数的增加&#xff0c;宿舍管理成为高校管理中的重要问题。本论文旨在研究玉林师范学院宿舍管理系统&#xff0c;探讨其优势和不足&#xff0c;并提出改进建议。通过对相关文献的综述和实地调研&#xff0c;我们发现该系统在宿舍分配、卫生评分、失物招领、设施…

什么是 URL ?

统一资源定位符&#xff08;URL&#xff09;是一个字符串&#xff0c;它指定了一个资源在互联网上的位置以及如何访问它。URL 是由几部分组成的&#xff0c;每部分都有其特定的作用&#xff1a; 协议/方案&#xff1a;这是 URL 的开头部分&#xff0c;表明了用于访问资源的协议…

基于uniapp(vue3)H5附件上传组件,可限制文件大小

代码&#xff1a; <template><view class"upload-file"><text>最多上传5份附件&#xff0c;需小于50M</text><view class"" click"selectFile">上传</view></view><view class"list" v…

WPF自定义模板--TreeView 实现菜单连接线

有些小伙伴说&#xff0c;在TreeView中&#xff0c;怎么每一个都加上连接线&#xff0c;进行显示连接。 代码和效果如下&#xff1a; 其实就是在原来的模板中增加一列显示线条&#xff0c;然后绘制即可 <Window x:Class"XH.TemplateLesson.TreeViewWindow"xmln…

无法定位程序输入点Z9 qt assertPKcS0i于动态链接库F:\code\projects\06_algorithm\main.exe

解决方法&#xff1a; 这个报错&#xff0c;是因为程序在运行时没要找到所需的dll库&#xff0c;如果把这个程序方法中对应库的目录下执行&#xff0c;则可正常执行。即使将图中mingw_64\bin 环境变量上移到msvc2022_64\bin 之前也不可以。 最终的解决方法是在makefile中设置环…

vue组件深入介绍之插槽

了解插槽之前请先了解vue组件基础及注册 Vue2官网介绍 Vue3官网介绍 1、vue2插槽介绍 在2.6.0中&#xff0c;具名插槽和作用域插槽引入了一个新的统一语法&#xff08;v-slot指令&#xff09;。它将取代slot和slot-scope&#xff1b; Vue 实现了一套内容分发的 API&#xf…

等保2.0 实施方案之信息软件验证要求

一、等保2.0背景及意义 随着信息技术的快速发展和网络安全威胁的不断演变&#xff0c;网络安全已成为国家安全、社会稳定和经济发展的重要保障。等保2.0&#xff08;即《信息安全技术 网络安全等级保护基本要求》2.0版本&#xff09;作为网络安全等级保护制度的最新标准&#x…

Revit 专业实用的BIM模型设计软件下载安装,Revit 最新版下载安装

Revit&#xff0c;该软件是专门为建筑信息模型&#xff08;BIM&#xff09;量身打造的&#xff0c;不仅极大提升了建筑设计师的工作效率&#xff0c;更为他们创造了一个更加精确、高效的设计环境。 在Revit的助力下&#xff0c;建筑设计师们能够轻松地进行建筑建模&#xff0c…

AI:开发者的助力还是终结者?

作为一名科技工作研发者&#xff0c;在科技浪潮汹涌澎湃的当下&#xff0c;AI 对于开发者的角色定位成为了一个备受瞩目的焦点话题。 AI 是在助力开发者&#xff0c;还是会取而代之&#xff1f;让我们从技术的角度深入剖析。 不可否认&#xff0c;AI 为开发者带来了前所未有的便…

Django QuerySet对象,all()方法

all()方法 在Django中&#xff0c;all()方法是QuerySet对象的一个方法&#xff0c;用于获取模型的所有实例。 当你调用ModelName.objects.all()时&#xff0c;Django会生成一个SQL查询&#xff0c;从数据库中获取该模型的所有记录&#xff0c;并返回一个QuerySet对象&#xf…

uniapp + vite中 uni.scss 使用 /deep/ 不生效(踩坑记录三)

vite 中使用 /deep/ 进行样式穿透报错 原因&#xff1a;vite 中不支持&#xff0c;换成 ::v-deep 或:deep即可