Webpack: Dependency Graph 管理模块间依赖

概述

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.mfbz.cn/a/768015.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Steam夏促怎么注册 Steam夏促账号注册教程

随着夏日的炙热渐渐充斥着每一个角落&#xff0c;Steam平台也赶来添热闹&#xff0c;推出了一系列让人眼前一亮的夏季促销活动。如果你也是游戏爱好者&#xff0c;我们肯定不能错过这次的steam夏促。正直本次夏日促销有着很多的游戏迎来史低和新史低&#xff0c;有各种各样的游…

智慧城市大数据平台总体技术建设方案(DOC文件)

第1章 总体说明 1.1 建设背景 1.2 建设目标 1.3 项目建设主要内容 1.4 设计原则 第2章 对项目的理解 2.1 现状分析 2.2 业务需求分析 2.3 功能需求分析 第3章 大数据平台建设方案 3.1 大数据平台总体设计 3.2 大数据平台功能设计 3.3 平台应用 第4章 政策标准保障…

声音音频文件波谱可视化展示

1、简单图形展示 import matplotlib.pyplot as plt import numpy as np import torch import torchaudiodef plot_waveform(waveform, sample_rate, title"Waveform", xlimNone, ylimNone):waveform waveform.numpy()num_channels, num_frames waveform.shapetime…

JVM原理(十二):JVM虚拟机类加载过程

一个类型从被加载到虚拟机内存中开始&#xff0c;到卸载为止&#xff0c;它的整个生命周期将会经过 加载、验证、准备、解析、初始化、使用、卸载七个阶段。其中 验证、准备、解析三个部分统称为 连接 1. 加载 加载是整个类加载的一个过程。在加载阶段&#xff0c;Java虚拟机…

【测试】五子棋项目测试报告

目录 一、项目概述及测试目标 二、项目功能 三、测试类型 1&#xff09;功能测试 ​编辑 2&#xff09;自动化测试 四、测试总结 一、项目概述及测试目标 本项目是一个基于Web的五子棋实时对战应用&#xff0c;旨在提供用户之间的多人实时游戏体验。项目采用前…

springboot微信小程序的化妆品商城系统-计算机毕业设计源码041152

摘 要 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;微信小程序的化妆品商城系统被用户普遍使用&#xff0c;为…

超声波俱乐部分享:万物智能——AI重新定义交互体验

6月25日&#xff0c;第十八期超声波俱乐部内部分享会在杭州圆满落幕&#xff0c;本期的主题是&#xff1a;万物智能——AI重新定义交互体验。 到场的嘉宾有&#xff1a;超声波创始人杨子超&#xff0c;超声波联合创始人、和牛商业创始人刘思雨&#xff0c;上海非著名资深程序员…

centos编译内核ko模块

1、make报错 make: * /lib/modules/4.14.0-49.el7a.aarch64/build: 没有那个文件或目录。 停止。 [rootlocalhost 4.14.0-49.el7a.aarch64]# pwd /lib/modules/4.14.0-49.el7a.aarch64 [rootlocalhost 4.14.0-49.el7a.aarch64]# ll 总用量 1744 lrwxrwxrwx. 1 root root …

小程序-<web-view>嵌套H5页面支付功能

背景&#xff1a;小程序未发布前&#xff0c;公司使用vue框架搭建了管理系统&#xff0c;为了减少开发成本&#xff0c;微信提供了web-view来帮助已有系统能在小程序上发布&#xff0c;详见web-view | 微信开放文档。因公司一直未打通嵌套H5小程序的支付功能&#xff0c;导致用…

Android 输入系统 InputStage

整体流程如上所说&#xff0c;简要归纳如下&#xff1a; 输入法之前的处理 输入法处理 输入法之后处理 综合处理 InputStage将输入事件的处理分成若干个阶段&#xff08;Stage&#xff09;, 如果当前有输入法窗口&#xff0c;则事件处理从 NativePreIme 开始&#xff0c;否…

Flink 窗口触发器(Trigger)(一)

Flink 窗口触发器(Trigger)(一) Flink 窗口触发器(Trigger)(二) Flink的窗口触发器&#xff08;Trigger&#xff09;是流处理中一个非常关键的概念&#xff0c;它定义了窗口何时被触发并决定触发后的行为&#xff08;如进行窗口数据的计算或清理&#xff09;。 一、基本概念 …

AC7801时钟配置流程

一 默认配置 在启动文件中&#xff0c;已经对时钟进行了初始化&#xff0c;默认按外部8M晶振&#xff0c;配置系统时钟为48MHZ&#xff0c;APB为系统时钟的2分频&#xff0c;为24MHZ。在system_ac780x.c文件中&#xff0c;可以找到下面这个系统初始化函数&#xff0c;里面有Se…

力扣hot100-普通数组2

文章目录 题目&#xff1a;轮转数组方法1-使用额外的数组方法2-三次反转数组 除自身以外数组的乘积方法1-用到了除法方法2-前后缀乘积法 题目&#xff1a;轮转数组 原题链接&#xff1a;轮转数组 方法1-使用额外的数组 方法1是自己写出来的。方法2参考的别人的&#xff0c;…

守护创新之魂:源代码防泄漏的终极策略

在信息化快速发展的今天&#xff0c;企业的核心机密数据&#xff0c;尤其是源代码&#xff0c;成为了企业竞争力的关键所在。然而&#xff0c;源代码的泄露风险也随之增加&#xff0c;给企业的安全和发展带来了巨大威胁。在这样的背景下&#xff0c;SDC沙盒作为一种创新的源代码…

C++——stack和queue类用法指南

一、stack的介绍和使用 1.1 stack的介绍 1、stack是一种容器适配器&#xff0c;专门用在具有后进先出操作的上下文环境中&#xff0c;其删除只能从容器的一端进行插入与提取操作 2、stack是作为容器适配器被实现的&#xff0c;容器适配器即是对特定类封装作为其底层的容器&am…

imx6ull/linux应用编程学习(8)PWM应用编程(基于正点)

1.应用层如何操控PWM&#xff1a; 与 LED 设备一样&#xff0c; PWM 同样也是通过 sysfs 方式进行操控&#xff0c;进入到/sys/class/pwm 目录下 这里列举出了 8 个以 pwmchipX&#xff08;X 表示数字 0~7&#xff09;命名的文件夹&#xff0c;这八个文件夹其实就对应了…

同样的APP为何在Android 8以后网络感觉变卡?

前言 在无线网络技术不断发展的今天&#xff0c;Wi-Fi已经成为了我们日常生活中不可或缺的一部分。无论是家庭娱乐、办公还是在线游戏&#xff0c;Wi-Fi都在提供着便捷的互联网接入服务。然而&#xff0c;在安卓8.1后&#xff0c;为了进一步延长安卓设备的待机时间。原生安卓(A…

Ubuntu18.04新安装--无网络连接、重启黑屏解决教程

一、安装Ubuntu Ubuntu安装需要U盘作为启动盘&#xff0c;在目前教新的电脑中选中GPT作为分区&#xff0c;制作启动盘&#xff0c;其中在安装双系统Ubuntu时&#xff0c;以自定义格式作为存储空间。详细安装过程以以及如何分区请参考下列链接&#xff1a;内含详细安装过程&…

不是大厂云用不起,而是五洛云更有性价比

明月代维的一个客户的大厂云境外云服务器再有几天就到期了&#xff0c;续费提醒那是提前一周准时到来&#xff0c;但是看到客户发来的续费价格截图&#xff0c;我是真的没忍住。这不就是在杀熟吗&#xff1f;就这配置续费竟然如此昂贵&#xff1f;说实话这个客户的服务器代维是…

哈哈看到这条消息感觉就像是打开了窗户

在这个信息爆炸的时代&#xff0c;每一条动态可能成为我们情绪的小小触发器。今天&#xff0c;当我无意间滑过那条由杜海涛亲自发布的“自曝式”消息时&#xff0c;不禁心头一颤——如果这是我的另一半&#xff0c;哎呀&#xff0c;那画面&#xff0c;简直比烧烤摊还要“热辣”…