likes
comments
collection
share

Webpack5源码解读系列2 - 构建应用模块拓扑图

作者站长头像
站长
· 阅读数 70

概念

官网描述

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, only one - to be loaded by the browser.

上面这段官方描述我们可以提炼出以下信息:在Webpack中,一个文件依赖了另外一个文称之为dependency(依赖),文件可以是代码资源或者非代码资源。在开始编译时,Webpack会从入口开始递归遍历项目,获取所有模块的依赖,构建一个dependency graph(依赖拓扑图),进而根据这些依赖打包项目并用于浏览器运行。

Webpack中每个文件被视为一个模块,应用中以Module领域模型描述,而模块之间的引用(即依赖)由Dependency领域模型描述,Webpack应用构建时遍历Module及其依赖,从而构建出项目文件拓扑图ModuleGraphWebpack5之前使用DependencyGraph描述项目文件拓扑)。这里可以先行列出领域模型概念:

Module

Module是文件在内存中的模型映射,描述文件位置、唯一标识、依赖以及后续代码生成等,同时Module是一个基础类,不同类型模块通过继承可按需实现模块能力,如NormalModule描述项目文件,RuntimeModule描述运行时能力。

Dependency

Module使用了导入或者导出语法时,会被解析为Dependency,描述Module之间的引用关系,同时Dependency也是基类,通过继承可实现不同的导入导出语法,如cjs、amd、es6模块导入导出均需要实现对应Dependency实现类。

ModuleGraph

从项目入口文件开始解析节点(Module)和边(Dependency)构成一张拓扑图,并最终组装成为ModuleGraph

模块分析过程

入口插件

Webpack通过插件机制极大提高了构建工具灵活性,编译开始时通过EntryPlugin往编译器执行队列中添加入口依赖:

class EntryPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap(
      "EntryPlugin",
      (compilation, { normalModuleFactory }) => {
        // 设置 EntryDependency -> 模块构建工厂函数,确定Dependency变成什么实例
        compilation.dependencyFactories.set(
          EntryDependency,
          normalModuleFactory
        );
      }
    );

    const { entry, options, context } = this;
    const dep = EntryPlugin.createDependency(entry, options);

    // make阶段开始时,往构建任务注入入口
    compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
      compilation.addEntry(context, dep, options, err => {
        callback(err);
      });
    });
  }

  static createDependency(entry, options) {
    // 将入口配置处理成 EntryDependency
    const dep = new EntryDependency(entry);
    dep.loc = { name: typeof options === "object" ? options.name : options };
    return dep;
  }
}

为什么Webpack不将入口Dependency创建逻辑放到构建任务中,而是将其通过插件机制解耦出去呢?这是因为入口配置除了可以被EntryPlugin视为普通入口,还可以通过使用DllPlugin处理为动态链接库,所以这里由插件完成入口处理逻辑。

模块解析

模块解析过程分为模块创建、模块构建、依赖获取三个阶段:

  1. 模块创建

Dependency(即依赖)分为很多类型,在代码上以继承Dependency类实现,常用类型有:

  • ModuleDependency:处理模块导入依赖,具体为模块导入依赖语法;
  • ContextDependency:处理模块目录上下,如require.context语法读取目录内容;
  • NullDependency:空依赖类型,处理模块导出;

模块解析过程中需要递归遍历模块所有依赖,所以使用到了ModuleDependency,但并不是直接使用ModuleDependency,而是通过其实现类完成领域模型建立(没错ModuleDependency只是中间基类)。这是因为Webpack需要处理包含esmcjsamd在内多种模块管理能力,需要为各种模块处理语法预设解析逻辑,所以需要分别为其编写实现类,所以继承关系如下:

Webpack进入make阶段之后,入口插件会向Compilation插入多一个入口依赖,这便是应用编译的开始。拿到Dependency之后Webpack会获取到Dependency对应的模块创建工厂函数,如果是普通代码文件,那么便是NormalModuleFactory。这些Dependency和工厂函数的映射维护在Compilation中,在初始化时由各个插件注册维护,可参考上面的入口插件一段代码:

compilation.dependencyFactories.set(
  EntryDependency,
  normalModuleFactory
);

获取到模块创建工厂函数之后,通过工厂函数创建模块实例,具体模块创建流程如下:

Webpack5源码解读系列2 - 构建应用模块拓扑图

  1. 模块构建

类似于Dependency,模块也存在很多类型,如普通文件资源处理为NormalModuleWebpack运行时代码资源处理为RuntimeModule、远程模块处理为RemoteModule。以NormalModule为例子,模块构建时调用NormalModulebuild方法,构建时需要经历文件读取、文件预处理、文件解析三个阶段:

  1. 文件读取:读取文件代码内容;
  2. 文件预处理:使用loader转换文件内容,如转换js语法、将非js文件转为js可识别内容;
  3. 文件解析:使用解析器将文件内容解析为抽象语法树,如js模块使用JS解析器、css模块使用CSS解析器、WASM模块使用WASM解析器;

Webpack5源码解读系列2 - 构建应用模块拓扑图

NormalModule构建有两个目的:

  • 文件内容预处理:使用loader处理语法糖、特殊文件内容;
  • 语法转译:文件解析成抽象语法树之后,通过遍历抽象语法树并抛出事件,由插件捕获并在代码生成阶段做语法转译操作。
  1. 模块依赖获取

解析器解析模块代码时会不断抛出事件,各种模块语法插件通过监听模块导入事件从而创建出本模块所有Dependency,如下面模块语法会解析出多个Dependency

Webpack5源码解读系列2 - 构建应用模块拓扑图Webpack5源码解读系列2 - 构建应用模块拓扑图

模块构建完毕后,已经确定了本模块以及相关联的依赖,此时会以Module映射为ModuleGraphModule的节点,DependencyModuleGraphConnection的边填充ModuleGraph,逐步构建模块拓扑图:

Webpack5源码解读系列2 - 构建应用模块拓扑图

填充完ModuleGraph之后,会遍历本模块的Dependency进入下一轮模块解析。

构建流程

模块解析这一段讲解了一个模块从依赖到模块的构建过程,是一个递归构建任务过程,由于在模块解析过程中存在需要文件读写操作,递归构建任务时性能会非常低,所以在真实场景中Webpack使用了AsyncQueue允许多个任务并发处理,从而提高构建速度。

AsynQueue介绍

Webpack提供了很多种异步任务执行模型,如AsyncQueueAsyncTree,为异步场景提供任务执行调度能力,AsyncQueue简单理解为带优先级别、限制任务数量异步任务执行队列。在构建阶段使用了factoriesModuleaddModulebuildModuleprocessModuleDependencies四个异步队列,分别为模块创建、模块添加到队列、模块构建、解析模块依赖提供执行队列。

先介绍AsyncQueue内部执行机制:

  • 子队列:子队列优先级别低于父队列,当父队列为空时才会将子队列内容取出执行,即存在优先级别;
  • parallelism:提供并发执行数量;

Webpack5源码解读系列2 - 构建应用模块拓扑图

四个队列的优先级别分别按照factoriesModule->addModule->buildModule->processModuleDependencies从高顺序顺序排列,这样处理的用意是什么呢?

首先看一下AsyncQueue的遍历模式:图数据结构一般使用DFS和BFS遍历方式,AsyncQueue可以理解为优化的BFS,我们以普通的图遍历来看:

Webpack5源码解读系列2 - 构建应用模块拓扑图

按照普通BFS,那么遍历顺序是[A, B, C, D, E],如果遍历到C时需要花费大量时间,那么需要等待。而使用AsyncQueue时,如果C花费大量时间,那么会先处理DE,这是使用AsyncQueue的好处。增加优先级级别目的是在大多数情况下尽可能让四个 队列 都处于工作状态,由于遍历是从根节点开始,一般情况下越靠近根节点,那么层级就越高,而越高层级的节点处理完毕后产生的子树节点就会越多。

递归构建过程

在基于上述模块解析、AsyncQueue队列执行过程我们可以得出下面的构建流程图:

Webpack5源码解读系列2 - 构建应用模块拓扑图

case解析

我们以一个简单例子解析一下构建结果:

Webpack5源码解读系列2 - 构建应用模块拓扑图

构建顺序如下:

  1. EntryPlugin调用addEntry,构建模块A;
  2. 获得模块A的依赖,将B、C模块推入构建队列中,等待下一轮的构建
  3. 以此类推完成D、E模块构建

构建完成后,ModuleGraph会有如下数据:

const ModuleGraph = {
  // Module -> ModuleGraphModule
  _moduleMap: {
    [Module<A>]: {
      inComingConnections: Set([
        // ModuleGraphConnection<OriginModule, Module>,OriginModule为边起点节点,Module为边的终点节点
        ModuleGraphConnection<null, A>
      ]),
      outGoingCnnections: Set([
        ModuleGraphConnection<A, B>,
        ModuleGraphConnection<A, C>
      ])
    },
    [Module<B>]: {
      inComingConnections: Set([
        ModuleGraphConnection<A, B>
      ]),
      outGoingCnnections: Set([
        ModuleGraphConnection<B, E>,
      ])
    },
    [Module<C>]: {
      inComingConnections: Set([
        ModuleGraphConnection<A, C>
      ]),
      outGoingCnnections: Set([
        ModuleGraphConnection<C, D>,
      ])
    },
    [Module<D>]: {
      inComingConnections: Set([
        ModuleGraphConnection<C, D>
      ]),
      outGoingCnnections: Set([
        ModuleGraphConnection<D, E>,
      ])
    },
    [Module<E>]: {
      inComingConnections: Set([
        ModuleGraphConnection<B, E>,
        ModuleGraphConnection<D, E>
      ]),
      outGoingCnnections: Set([])
    },
  }
}

小结

本文介绍了几个关键领域模型:

  • Module:文件/资源在Webpack中的映射,存在多种类型的Module,如:

    • 普通文件资源处理为NormalModule
    • Webpack运行时代码资源处理为RuntimeModule
    • 远程模块处理为RemoteModule
  • Dependency:模块依赖的描述,主要存在三种类型:

    • ModuleDependency:处理模块导入依赖,具体为模块导入依赖语法;
    • ContextDependency:处理模块目录上下,如require.context语法读取目录内容;
    • NullDependency:空依赖类型,处理模块导出;
  • ModuleGraph:项目文件引用关系在Webpack中的映射拓扑图,内部由ModuleGraphModuleModuleGraphConnection组成:

    • ModuleGraphModuleModuleModuleGraph的映射;
    • ModuleGraphConnectionDependencyModuleGraph的映射;

本文还介绍了项目模块构建流程,使用AsyncQueue提高构建速度。

转载自:https://juejin.cn/post/7231805092272373815
评论
请登录