webpack初步理解

Webpack 是一个非常强大且有趣的工具,它被视为当今许多 Web 开发人员用来构建其应用程序的基础组件。然而,许多人会认为使用它是一个相当大的挑战,主要是因为它的复杂性

 

webpack bundle过程图表

modules

模块是一个文件的升级版本。一个模块,一旦创建并且构建之后,除了包含原始的文件代码之外,还有一些其他有意义的信息:比如模块使用的加载器,模块的依赖项,模块的导出(如果存在的话)以及模块的hash值等等。

The  entry  object

Entry对象(也可以称为入口对象)记住一点:enrty对象中的每一项都是模块树中的根模块。模块树,因为根模块可能需要一些其他模块(又称为依赖项),这些模块(依赖项也是模块)可能也需要其他模块等等,因此您可以在更高的级别上理解如何构建这棵模块树。所有这些模块树都存储联结在一个 ModuleGraph(模块图) 中。

另外一点我们需要知道的是:webpack 是建立在许多插件之上的。尽管bundle过程已经构建起来,但是可以嵌入很多方法来添加自定义逻辑。webpack的扩展通过hooks来实现。比如,你可以在模块图已经构建之后,或者为chunk生成新的assets 时或者模块将要构建前(加载器运行时、解析源代码时),添加一些自定义逻辑等等。hooks非常有趣,可以为许多与 webpack 定制相关的问题提供解决方案。大多数时候,hooks是根据它们的功能分组的,每一个插件都有明确定义的功能。例如,有一个插件负责处理 import() 函数(负责解析注释和参数)它被称为 ImportParserPlugin,它所做的只是在 AST 解析期间遇到 import() 调用时添加一个hook。

同时不足为奇的是,有几个负责处理entry对象的插件。有一个 EntryOptionPlugin,它实际上接受entry对象为参数并为入口对象中的每个项目(entry item)创建一个 EntryPlugin。这部分很重要:入口对象的每一项都将产生一棵模块树(所有这些模块树都是彼此分离的)。基本上,EntryPlugin 开启每一棵模块树的创建过程,每个模块树都会将信息添加到同一个地方,即 ModuleGraph。因此,我们会说 EntryPlugin 开启了这个复杂的处理过程。

入口对象插件处理

结合初始图片来看,我们可以知道,EntryPlugin 也是创建 EntryDependency 的地方。基于上图,让我们通过自己实现EntryOptionsPlugin来进一步了解EntryOptionsPlugin的重要性:

class CustomEntryOptionPlugin {
  // This is the standard way of creating plugins.
  // It's either this, or a simple function, but we're using this approach
  // in order to be on par with how most of the plugins are created.
  apply(compiler) {
    // Recall that hooks offer us the possibility to intervene in the
    // bundling process.
    // With the help of the `entryOption` hook, we're adding the logic
    // that will basically mean the start of the bundling process. As in,
    // the `entryObject` argument will hold the `entry` object from the
    // configuration file and we'll be using it to set up the creation of
    // module trees.
    compiler.hooks.entryOption.tap('CustomEntryOptionPlugin', entryObject => {
      // The `EntryOption` class will handle the creation of a module tree.
      const EntryOption = class {
        constructor (options) {
          this.options = options;
        };

        // Since this is still a plugin, we're abiding by the standard.
        apply(compiler) {
          // The `start` hook marks the start of the bundling process.
          // It will be called **after** `hooks.entryOption` is called.
          compiler.hooks.start('EntryOption', ({ createModuleTree }) => {
            // Creating new tree of modules, based on the configuration of this plugin.
            // The `options` contain the name of the entry(which essentially is the name of the chunk)
            // and the file name.
            // The `EntryDependency` encapsulates these options and also provides way to
            // create modules(because it maps to a `NormalModuleFactory`, which produces `NormalModule`s).
            // After calling `createModuleTree`, the source code of the file will be found,
            // then a module instance will be created and then webpack will get its AST, which 
            // will be further used in the bundling process.
            createModuleTree(new EntryDependency(this.options));
          });
        };
      };

      // For each item in the `entryObject` we're preparing
      // the creation of a module tree. Remember that each
      // module tree is independent of others.
			// The `entryObject` could be something like this: `{ a: './a.js' }`
      for (const name in entryObject) {
        const fileName = entryObject[name];
        // We're fundamentally saying: `ok webpack, when the bundling process starts,
        // be ready to create a module tree for this entry`.
        new EntryOption({ name, fileName }).apply(compiler);
      };
    });
  }
};

在本节的最后一部分,我们将稍微扩展介绍什么是依赖(Dependency),因为我们将在本文中进一步使用它。您现在可能想知道 EntryDependency 是什么以及为什么需要它。从我的角度来看,当创建新模块时,它可以归结为一种智能抽象。简单地说,依赖就是实际模块实例的准备阶段(初级阶段)。例如,甚至entry对象的项目从webpack 的视角来看也是依赖项,它们指明要创建模块实例的最低限度要求:它的路径(例如 ./a.js、./b.js)。没有依赖项就无法创建模块,因为依赖项包含模块的请求以及其他重要信息,即可以找到模块的源文件路径(例如'./a.js')。依赖项还指示如何构造该模块,它怎样使用一个模块工厂(module factory)来完成模块构建。模块工厂知道如何从原始状态(字符串源代码)转化到一些可由webpack使用的具体实体。EntryDependency 实际上是 ModuleDependency 的一种类型,这意味着它肯定会保持模块的请求,并且它指向的模块工厂是 NormalModuleFactory。然后,NormalModuleFactory 确切地知道要做什么才能从一条路径创建对 webpack 有意义的东西。另一种思考方式是,一个模块起初只是一个简单的路径(在入口对象中或导入语句的一部分中),然后它成为一个依赖项,最后成为一个模块。 这是一种可视化的方法:

因此,在创建模块树的根模块时,最开始会使用EntryDependency。

对于其他的模块,它们是其他类型的依赖项。例如,如果您使用import语句,如 import defaultFn from './a.js' ,那么将有一个HarmonyImportSideEffectDependency保存模块的请求(在本例中为 './a.js')并映射到NormalModuleFactory。因此,文件“a.js”将会是一个新模块,希望现在您可以理解依赖项所起的重要作用。它们本质上是指导 webpack 如何创建模块。我们将在本文后面揭示有关依赖项的更多信息。

快速回顾一下我们在本节中学到的内容:

1、对于 entry 对象中的每一项,都会有一个 EntryPlugin 实例,在该实例中创建了一个 EntryDependency。这个 EntryDependency 保存模块的请求(即文件的路径),并且还提供了一种通过映射到模块工厂(即 NormalModuleFactory)来充分利用该请求。模块工厂知道如何仅从文件路径创建对 webpack 有用的实体。

2、再次,依赖关系对于创建模块至关重要,因为它包含重要信息,例如模块的请求以及如何处理该请求。有几种类型的依赖关系,并非所有类型的依赖都对创建新模块有用。从每个 EntryPlugin 实例出发并在新创建的 EntryDependency 的帮助下,将创建一个模块树。模块树建立在模块及其依赖关系之上,这些依赖项也是模块,也可以有依赖关系。

现在,让我们通过了解有关 ModuleGraph 的更多信息来继续我们的学习之旅。

 

理解 ModuleGraph

ModuleGraph 是一种跟踪已经构建好的模块的方法。它特别依靠依赖关系,因为它们提供了连接 2 个不同模块的方法。例如:

// a.js
import defaultBFn from './b.js';
// b.js
export default function () { console.log('Hello from B!'); }

这里我们有 2 个文件,所以有 2 个模块。文件 a 需要文件 b 中的某些内容,因此在 a 中存在由 import 语句建立的依赖关系。就 ModuleGraph 而言,依赖项定义了一种连接 2 个模块的方式。甚至上一节中的 EntryDependency 也连接了 2 个模块:图的根模块,我们将其称为空模块,以及与入口文件关联的模块。上面的代码片段可以可视化如下:

 

阐明简单模块(即 NormalModule 实例)和属于 ModuleGraph 的模块之间的区别很重要。ModuleGraph 的节点称为 ModuleGraphModule(这里是不是应该称呼:模块图模块),它只是一个修饰的 NormalModule 实例。ModuleGraph 借助具有以下签名的映射跟踪这些装饰模块:

 Map<Module, ModuleGraphModule>

这些方面是有必要提及的,因为如果只有 NormalModule 实例,那么您对它们无能为力,它们不知道如何相互通信。ModuleGraph 赋予这些裸模块意义,通过在上述映射的帮助下互连它们,该映射为每个 NormalModule 分配一个 ModuleGraphModule。这将在构建 ModuleGraph 部分很有意义,我们将使用 ModuleGraph 及其内部映射来遍历模块图。。我们将属于 ModuleGraph 的模块简称为模块,因为区别仅包含几个附加属性。

对于属于 ModuleGraph 的节点,是有少有的几个定义好的东西:传入连接和传出连接。Connection是 ModuleGraph 的另一个小实体,它包含有意义的信息,例如:源模块、目标模块和连接前面提到的 2 个模块的依赖项。具体来说,根据上图,新建了一个连接:

// This is based on the diagram and the snippet from above.
Connection: {
	originModule: A,
	destinationModule: B,
	dependency: ImportDependency
}

并且上面的连接将被添加到 A.outgoingConnections 集合和 B.incomingConnections 集合中。

这些是 ModuleGraph 的基本概念。正如上一节中已经提到的,从entry创建的所有模块树都会将有意义的信息输出到同一个地方,即 ModuleGraph。这是因为所有这些模块树最终都会与空模块(ModuleGraph 的根模块)相连。通过 EntryDependency 和从入口文件创建的模块建立与空模块的连接。这是我对 ModuleGraph 的看法:

如您所见,空模块与从entry对象中的项目生成的每个模块树的根模块都有一个连接。图中的每条边代表 2 个模块之间的连接,每个连接都包含有关源节点、目标节点和依赖项的信息(这非正式地回答了为什么这 2 个模块连接的问题?)。

现在我们对 ModuleGraph 有点熟悉了,让我们看看它是如何构建的。

构建 ModuleGraph

正如我们在上一节中看到的,ModuleGraph 以一个空模块开始,其直接后代是模块树的根模块,这些模块树是从entry对象项构建的。因此,为了了解 ModuleGraph 是如何构建的,我们将研究单个模块树的构建过程。

第一个模块的创建

我们将从一个非常简单的entry对象开始:

entry: {
	a: './a.js',
}

根据第一部分所说的,在某些时候,我们最终会得到一个请求为“./a.js”的EntryDependency。这个 EntryDependency 提供了一种从该请求创建有意义的东西的方法,因为它映射到一个模块工厂,即 NormalModuleFactory。这是我们在第一部分中没提到的地方。

该过程的下一步是 NormalModuleFactory 起作用的地方。NormalModuleFactory,如果它成功完成它的任务,将创建一个 NormalModule为了确保没有不确定性,NormalModule 只是文件源代码的反序列化版本,它只不过是一个原始字符串。原始字符串不会带来太多价值,因此 webpack 不能用它做太多事情。NormalModule 还将源代码存储为字符串,但同时,它还将包含其他有意义的信息和功能,例如:应用到它的加载器,构建模块的逻辑,生成运行时代码的逻辑,它的哈希值等等。换句话说,从 webpack 的角度来看,NormalModule 是一个简单原始文件的有用版本

为了让 NormalModuleFactory 输出一个 NormalModule,它必须经过一些步骤。创建模块后还有一些事情要做,例如构建模块并处理其依赖项(如果有的话)。

这又是我们一直在关注的图表,现在专注于构建 ModuleGraph 部分:

NormalModuleFactory 通过调用它的 create 方法开始它的魔力。然后,process过程开始。这里是请求(文件的路径)被解析的地方,以及该类型文件的加载器。请注意,在此步骤中,将仅确定加载程序的文件路径,尚未调用加载程序。

模块构建处理

解析完所有必要的文件路径后,NormalModule创建完成。但是,在这一点上,该模块不是很有价值。构建模块后会出现很多相关信息。 NormalModule 的构建过程包括以下几个步骤:

  • 首先,将在原始源代码上调用加载器;如果有多个加载器,那么一个加载器的输出可能是另一个加载器的输入(在配置文件中提供加载器的顺序很重要);
  • 其次,通过所有加载器运行后的结果字符串将用 acorn(一个 JavaScript 解析器)解析,从而产生给定文件的 AST;
  • 最后分析AST;分析是必要的,因为在这个阶段会确定当前模块的依赖关系(例如其他模块),webpack 可以检测到它的神奇功能(例如 require.context、module.hot)等;AST 分析发生在 JavascriptParser 中,如果您单击链接,您应该会看到那里处理了很多案例;这部分过程是最重要的部分,因为捆绑过程中接下来的很多事情都取决于这部分;

通过生成的 AST 发现依赖关系

一种思考处理过程的方法,无需过多详细介绍,如下所示:

其中 moduleInstance 是指从 index.js 文件创建的 NormalModule。红色的 dep 指的是从第一个 import 语句创建的依赖项,蓝色的 dep 指的是第二个 import 语句。这只是查看事物的一种简化方式。实际上,如前所述,依赖项是在获得 AST 之后添加的。

现在已经检查了 AST,是时候继续构建我们在本节开头谈到的模块树的过程了。下一步是处理在上一步中找到的依赖项。如果我们按照上图,index 模块有两个依赖,它们也是模块,即 math.js 和 utils.js。但在依赖项成为实际模块之前,我们只有index模块,其 module.dependencies 有 2 个值,其中包含模块请求(文件路径)、导入说明符(例如 sum、greet)等信息。为了将它们变成模块,我们需要使用这些依赖关系映射到的 ModuleFactory 并重复上述相同的步骤(重复在本节开头的图中用虚线箭头表示)。在处理完当前模块的依赖关系之后,这些依赖关系可能也有依赖关系,并且这个过程一直持续到没有更多的依赖关系为止。这就是模块树的构建方式,当然还要确保正确设置父模块和子模块之间的连接。

根据我们到目前为止所获得的知识,我们自己实际试验 ModuleGraph 将是一个很好的练习。为此,让我们看看一种实现自定义插件的方法,该插件将允许我们遍历 ModuleGraph。这是描述模块如何相互依赖的图表:

为了确保图中的所有内容都可以理解,a.js 文件导入 b.js 文件,该文件同时导入 b1.js 和 c.js,然后 c.js 导入 c1.j 和 d.js,最后,d .js 导入 d1.js。最后,ROOT 指的是空模块,它是 ModuleGraph 的根。入口选项仅包含一个值 a.js:

// webpack.config.js
const config = {
  entry: path.resolve(__dirname, './src/a.js'),
	/* ... */
};

现在让我们看看我们的自定义插件是什么样子的:

// The way we're adding logic to the existing webpack hooks
// is by using the `tap` method, which has this signature:
// `tap(string, callback)`
// where `string` is mainly for debugging purposes, indicating
// the source where the custom logic has been added from.
// The `callback`'s argument depend on the hook on which we're adding custom functionality.

class UnderstandingModuleGraphPlugin {
  apply(compiler) {
    const className = this.constructor.name;
    // Onto the `compilation` object: it is where most of the *state* of
    // the bundling process is kept. It contains information such as the module graph,
    // the chunk graph, the created chunks, the created modules, the generated assets
    // and much more.
    compiler.hooks.compilation.tap(className, (compilation) => {
      // The `finishModules` is called after *all* the modules(including
      // their dependencies and the dependencies' dependencies and so forth)
      // have been built.
      compilation.hooks.finishModules.tap(className, (modules) => {
        // `modules` is the set which contains all the built modules.
        // These are simple `NormalModule` instances. Once again, a `NormalModule`
        // is produced by the `NormalModuleFactory`.
        // console.log(modules);

        // Retrieving the **module map**(Map<Module, ModuleGraphModule>).
        // It contains all the information we need in order to traverse the graph.
        const {
          moduleGraph: { _moduleMap: moduleMap },
        } = compilation;

        // Let's traverse the module graph in a DFS fashion.
        const dfs = () => {
          // Recall that the root module of the `ModuleGraph` is the
          // *null module*.
          const root = null;

          const visited = new Map();

          const traverse = (crtNode) => {
            if (visited.get(crtNode)) {
              return;
            }
            visited.set(crtNode, true);

            console.log(
              crtNode?.resource ? path.basename(crtNode?.resource) : 'ROOT'
            );

            // Getting the associated `ModuleGraphModule`, which only has some extra
            // properties besides a `NormalModule` that we can use to traverse the graph further.
            const correspondingGraphModule = moduleMap.get(crtNode);

            // A `Connection`'s `originModule` is the where the arrow starts
            // and a `Connection`'s `module` is there the arrow ends.
            // So, the `module` of a `Connection` is a child node.
            // Here you can find more about the graph's connection: https://github.com/webpack/webpack/blob/main/lib/ModuleGraphConnection.js#L53.
            // `correspondingGraphModule.outgoingConnections` is either a Set or undefined(in case the node has no children).
            // We're using `new Set` because a module can be reference the same module through multiple connections.
            // For instance, an `import foo from 'file.js'` will result in 2 connections: one for a simple import
            // and one for the `foo` default specifier. This is an implementation detail which you shouldn't worry about.
            const children = new Set(
              Array.from(
                correspondingGraphModule.outgoingConnections || [],
                (c) => c.module
              )
            );
            for (const c of children) {
              traverse(c);
            }
          };

          // Starting the traversal.
          traverse(root);
        };

        dfs();
      });
    });
  }
}

根据模块层次结构,运行 build 命令后,我们应该得到以下输出:

a.js
b.js
b1.js
c.js
c1.js
d.js
d1.js

现在已经构建了 ModuleGraph,希望您已经掌握了它,是时候了解接下来会发生什么了。根据主图,下一步将是创建块,所以让我们开始吧。但在此之前,有必要澄清一些重要的概念,例如 Chunk、ChunkGroup 和 EntryPoint。

 

澄清 Chunk,ChunkGroup,EntryPoint

现在我们对什么是模块有了一些了解,我们将在此基础上解释本节标题中提到的概念。为了再次快速解释什么是模块,只要知道模块是文件的升级版本就足够了。一个模块,一旦创建和构建,除了原始源代码之外,还包含许多有意义的信息,例如:使用的加载器、它的依赖项、它的导出(如果有的话)、它的哈希等等。

一个chunk封装一个或多个模块。乍一看,可能会认为entry文件的数量(一个entry文件=入口对象的一项)与生成的块的数量成正比。这个陈述部分正确,因为entry对象可能只有一个项目但是chunks的数量可能大于一个。确实,对于每个entry item,在 dist 目录中都会有一个相应的块,但是可以隐式创建其他块,例如在使用 import() 函数时。但是不管是怎么创建的,每个chunk都会在dist目录下有一个对应的文件。我们将在构建 ChunkGraph 部分对此进行展开说明,我们将阐明哪些模块属于一个块,哪些不属于?

一个 ChunkGroup 包含一个或多个chunk。一个 ChunkGroup可以是另一个 ChunkGroup的父级或子级。例如,当使用动态导入时,对于每个使用的 import() 函数,都会创建一个 ChunkGroup,其父级将是一个现有的 ChunkGroup,它包含使用 import() 函数的文件(即模块)。在构建 ChunkGraph 部分可以看到这一事实的可视化。

EntryPoint 是一种 ChunkGroup,它是为entry对象中的每个项目创建的。chunk属于entrypoint这一事实对渲染过程有影响,因为我们将在以后的文章中更清楚地说明这一点。

鉴于我们对这些概念比较熟悉,让我们继续了解 ChunkGraph

构建ChunkGraph

回想一下,到目前为止,我们所拥有的只是一个 ModuleGraph,我们在上一节中讨论过。但是,ModuleGraph 只是bundling过程的必要部分。必须利用它才能使代码拆分等功能成为可能。

在bundling过程的这一点上,对于来自entry对象的每个项目,都会有一个entrypoint。由于它是 ChunkGroup 的一种,因此它至少会包含一个 chunk。所以,如果 entry 对象有 3 个 item,就会有 3 个 EntryPoint 实例,每个实例都有一个 chunk,也叫 entrypoint chunk,名字就是 entry item key 的值。与入口文件关联的模块称为入口模块,它们中的每一个都将属于它们的 entrypoint chunk。它们很重要,因为它们是 ChunkGraph 构建过程的起点。请注意,一个块可以有多个入口模块:

// webpack.config.js
entry: {
  foo: ['./a.js', './b.js'],
}

在上面的示例中,将有一个名为 foo 的块(项目的键)将有 2 个入口模块:一个与 a.js 文件关联,另一个与 b.js 文件关联。当然,该块将属于基于entry item创建的 EntryPoint 实例。

在详细介绍之前,让我们举一个例子,我们将在此基础上讨论构建过程:

entry: {
    foo: [path.join(__dirname, 'src', 'a.js'), path.join(__dirname, 'src', 'a1.js')],
    bar: path.join(__dirname, 'src', 'c.js'),
}

此示例将包含前面提到的内容:ChunkGroups(以及因此动态导入)、chunks和entrypoint的父子关系。

ChunkGraph 以递归方式构建。它首先将所有入口模块添加到队列中。然后,当一个入口模块被处理时,这意味着它的依赖项(也是模块)将被检查,并且每个依赖项也将被添加到队列中。这不断重复,直到队列变空。这部分过程是访问模块的地方。然而,这只是第一部分。回想一下,ChunkGroups 可以是其他 ChunkGroups 的父/子。这些连接在第二部分中得到解决。例如,如前所述,动态导入(即 import() 函数)将产生一个新的子 ChunkGroup。用 webpack 的说法,import() 表达式定义了一个异步的依赖块。从我的角度来看,它被称为块,因为首先想到的是包含其他对象的东西。在 import('./foo.js').then(module => ...) 的情况下,很明显我们的意图是异步加载一些东西,很明显,为了使用模块变量,在实际模块可用之前,必须解析 foo(包括 foo 本身)的所有依赖项(即模块)。我们将在以后的文章中彻底讨论 import() 函数的工作原理以及它的特殊性(例如魔术注释和其他选项)。

如果这激发了您的好奇心,那么这里就是在 AST 分析期间创建块的位置。

总结 ChunkGraph 构建过程的源代码可以在这里找到。

现在,让我们看一下根据我们上面的配置创建的 ChunkGraph 的图表:

该图说明了 ChunkGraph 的一个非常简化的版本,但它应该足以突出显示结果Chunk和 ChunkGroup 之间的关系。我们可以看到 4 个块,所以会有 4 个输出文件。 foo 块将有 4 个模块,其中 2 个是入口模块。bar chunk 将只有 1 个入口模块,而另一个可以被视为普通模块。我们还可以注意到,每个 import() 表达式都会产生一个新的 ChunkGroup(其父级是 bar EntryPoint),其中涉及一个新的 chunk。

 

生成文件的内容是根据 ChunkGraph 确定的,所以这就是为什么它对整个打包过程非常重要。我们将在下一节简要讨论块资产(即生成的文件)。

在探索我们将使用 ChunkGraph 的实际示例之前,重要的是要提及它的一些特殊性。与 ModuleGraph 类似,属于 ChunkGraph 的节点称为 ChunkGraphChunk(读作属于 ChunkGraph 的块),它只是一个装饰块,这意味着它作为一些额外的属性,例如作为块的一部分的模块,块的入口模块等。就像 ModuleGraph 一样,ChunkGraph 借助具有以下签名的映射使用附加属性跟踪这些块:WeakMap<Chunk, ChunkGraphChunk>。与 ModuleGraph 的 map 相比,由 ChunkGraph 维护的这个 map 不包含有关 chunk 之间连接的信息。相反,所有必要的信息(例如它所属的 ChunkGroups)都保存在块本身中。请记住,Chunk在 ChunkGroups 中组合在一起,并且这些ChunkGroup之间可以存在父子关系(正如我们在上图中看到的那样)。模块不是这样,因为模块可以相互依赖,但是没有严格的父模块概念。

现在让我们尝试在自定义插件中使用 ChunkGraph,以便更好地理解它。请注意,我们正在考虑的这个例子是上图描述的例子:

const path = require('path');

// We're printing this way in order to highlight the parent-child
// relationships between `ChunkGroup`s.
const printWithLeftPadding = (message, paddingLength) => console.log(message.padStart(message.length + paddingLength));

class UnderstandingChunkGraphPlugin {
  apply (compiler) {
    const className = this.constructor.name;
    compiler.hooks.compilation.tap(className, compilation => {
      // The `afterChunks` hook is called after the `ChunkGraph` has been built.
      compilation.hooks.afterChunks.tap(className, chunks => {
        // `chunks` is a set of all created chunks. The chunks are added into
        // this set based on the order in which they are created.
        // console.log(chunks);
        
        // As we've said earlier in the article, the `compilation` object
        // contains the state of the bundling process. Here we can also find
        // all the `ChunkGroup`s(including the `Entrypoint` instances) that have been created.
        // console.log(compilation.chunkGroups);
        
        // An `EntryPoint` is a type of `ChunkGroup` which is created for each
        // item in the `entry` object. In our current example, there are 2.
        // So, in order to traverse the `ChunkGraph`, we will have to start
        // from the `EntryPoints`, which are stored in the `compilation` object.
        // More about the `entrypoints` map(<string, Entrypoint>): https://github.com/webpack/webpack/blob/main/lib/Compilation.js#L956-L957
        const { entrypoints } = compilation;
        
        // More about the `chunkMap`(<Chunk, ChunkGraphChunk>): https://github.com/webpack/webpack/blob/main/lib/ChunkGraph.js#L226-L227
        const { chunkGraph: { _chunks: chunkMap } } = compilation;
        
        const printChunkGroupsInformation = (chunkGroup, paddingLength) => {
          printWithLeftPadding(`Current ChunkGroup's name: ${chunkGroup.name};`, paddingLength);
          printWithLeftPadding(`Is current ChunkGroup an EntryPoint? - ${chunkGroup.constructor.name === 'Entrypoint'}`, paddingLength);
          
          // `chunkGroup.chunks` - a `ChunkGroup` can contain one or mode chunks.
          const allModulesInChunkGroup = chunkGroup.chunks
            .flatMap(c => {
              // Using the information stored in the `ChunkGraph`
              // in order to get the modules contained by a single chunk.
              const associatedGraphChunk = chunkMap.get(c);
              
              // This includes the *entry modules* as well.
              // Using the spread operator because `.modules` is a Set in this case.
              return [...associatedGraphChunk.modules];
            })
            // The resource of a module is an absolute path and
            // we're only interested in the file name associated with
            // our module.
            .map(module => path.basename(module.resource));
          printWithLeftPadding(`The modules that belong to this chunk group: ${allModulesInChunkGroup.join(', ')}`, paddingLength);
          
          console.log('\n');
          
          // A `ChunkGroup` can have children `ChunkGroup`s.
          [...chunkGroup._children].forEach(childChunkGroup => printChunkGroupsInformation(childChunkGroup, paddingLength + 3));
        };
        
		// Traversing the `ChunkGraph` in a DFS manner.
        for (const [entryPointName, entryPoint] of entrypoints) {
          printChunkGroupsInformation(entryPoint, 0);
        }
      });
    });
  }
}; 

这是您应该看到的输出:

Current ChunkGroup's name: foo;
Is current ChunkGroup an EntryPoint? - true
The modules that belong to this chunk group: a.js, b.js, a1.js, b1.js

Current ChunkGroup's name: bar;
Is current ChunkGroup an EntryPoint? - true
The modules that belong to this chunk group: c.js, common.js


    Current ChunkGroup's name: c1;
    Is current ChunkGroup an EntryPoint? - false
    The modules that belong to this chunk group: c1.js

    Current ChunkGroup's name: c2;
    Is current ChunkGroup an EntryPoint? - false
    The modules that belong to this chunk group: c2.js

我们使用缩进来区分父子关系。我们还可以注意到输出与图表一致,因此我们可以确定遍历的正确性。

生成Chunk Assets

值得一提的是,生成的文件不仅仅是原始文件的复制粘贴版本,因为为了实现其功能,webpack 需要添加一些自定义代码,以使一切按预期工作。

这就引出了 webpack 如何知道要生成什么代码的问题。好吧,这一切都从最基本(也是最有用的)层开始:模块。一个模块可以导出成员,导入其他成员,使用动态导入,使用 webpack 特定的功能(例如 require.resolve)等。根据模块的源代码,webpack 可以确定生成哪些代码以实现所需的功能。这发现在 AST 分析期间开始,在此发现依赖项。尽管到目前为止我们一直在交替使用依赖项和模块,但事情在幕后有点复杂。

例如,一个简单的 import { aFunction } from './foo' 将产生 2 个依赖项(一个用于 import 语句本身,另一个用于说明符,即 aFunction),将从中创建单个模块。另一个例子是 import() 函数。正如前面部分中提到的,这将导致异步的依赖块,其中一个依赖项是 ImportDependency,它特定于动态导入。这些依赖关系是必不可少的,因为它们带有一些关于应该生成什么代码的提示。例如, ImportDependency 知道要告诉 webpack 什么才能异步获取导入的模块并使用其导出的成员。这些提示可以称为运行时要求。例如,如果模块导出了它的一些成员,就会有一些依赖(回想一下我们现在不是指模块),即 HarmonyExportSpecifierDependency,它将通知 webpack 它需要处理导出成员的逻辑。

总而言之,一个模块将附带其运行时要求,这取决于该模块在其源代码中使用的内容。块的运行时要求将是属于该块的所有模块的所有运行时要求的集合。现在 webpack 知道了一个块的所有需求,它将能够正确地生成运行时代码。这也称为渲染过程,我们将在专门的文章中详细讨论。现在,了解渲染过程严重依赖 ChunkGraph 就足够了,因为它包含ChunkGroup(即 ChunkGroup、EntryPoint),其中包含Chunk,其中包含Module,以细粒度的方式,包含有关将由 webpack 生成的运行时代码的信息和提示。

版权声明:著作权归作者所有。

thumb_up 0 | star_outline 0 | textsms 0