[译]深度解析Webpack打包过程(2万字预警)

原文:angularindepth.com/posts/1482/…

Webpack 是一个功能非常强大和有趣的工具,可被视为现代许多 Web 开发人员构建应用程序的基础。然而,许多人认为,由于其复杂性,使用它进行开发颇具挑战。

在这一系列的文章中,我想分享关于 Webpack 内部工作的许多细节,希望这能让 webpack 更加易于上手。本文将作为未来更深入探讨 Webpack 各项功能文章的基础。你将学习到懒加载是怎么实现的,tree shaking 是如何工作的,以及某些 loader 是如何工作的等等。我这个系列的目标是让你在解决 Webpack 相关问题时更加得心应手。这篇文章的目标让你对整个 Webpack 的工作过程有个全面的了解,以便于你能够理解 webpack 的各个方面或者调试问题。

我们将从一张图开始,简要地呈现整个打包流程。我们省略了一些细节,这些将在未来的文章中详细讨论。然后,我们将深入讲解图中所示的特定步骤。在此过程中,我们将解释模块、chunk 等概念。此外,为了便于理解,我将利用图和简化的代码片段来代替部分源代码。同时,我也会提供源代码的链接,以便于深入探索与学习。

按照惯例,我们将 NormalModules 简称为模块。除此之外,还有其他类型的模块,比如在使用模块联邦(module federation)时的 ExternalModule,以及使用 require.context() 时的 ConcatenatedModule,它们将在其他文章中单独讨论。在本文中,我们将专注于 NormalModules

webpack 打包过程总览

[译]深度解析Webpack打包过程(2万字预警)

entry 对象

必须强调的是,一切都始于 entry 对象。正如你所预期的那样,它支持许多配置,这个话题值得单独的一篇文章来讨论。这就是为什么我们只考虑一个非常简单的示例,其中 entry 对象只是一组键值对集合:

// webpack.config.js
entry: {
    a: './a.js',
    b: './b.js',
    /* ... */
}

在 Webpack 中,一个模块对应一个文件。因此,在图中,'a.js' 将生成一个新模块,'b.js' 也是如此。现在,只需理解模块是文件的升级版即可。模块被创建和构建之后,不仅包括原始的源代码,还包含了许多重要的信息,包括使用的 loader、依赖项、导出(如果有)、哈希等等。entry 对象中的每个条目,都可以被认为是模块树中的根模块。之所以说是模块树,是因为根模块可能需要依赖其他模块,这些模块又可能需要其他模块,如此形成了一个层次结构。所有这些模块树都存储在 ModuleGraph 中,我们将在下一部分详细说明。

[译]深度解析Webpack打包过程(2万字预警)

为了与初始图表保持一致,值得一提的是,EntryPlugin也是创建EntryDependency的地方。 基于上述图表,让我们通过松散地实现EntryOptionsPlugin来获得更多的见解:

[译]深度解析Webpack打包过程(2万字预警)

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

理解 ModuleGraph

ModuleGraph 它负责追踪并记录模块在构建过程中的依赖关系,定义连接两个模块的方式。例如:

// a.js
import defaultBFn from '.b.js/';

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

在这里,我们有两个文件,也就是两个模块。文件 a 依赖文件 b,因此在文件 a 中存在一个通过 import 语句建立的依赖关系。在 ModuleGraph 中,依赖关系是用来建立两个模块之间连接的一种机制。甚至在前一节中提到的 EntryDependency,也是在连接两个模块:模块图的根模块(我们可以称之为 null 模块),以及与入口文件相关联的模块。可以通过下面的图来理解上述代码片段:

[译]深度解析Webpack打包过程(2万字预警)

我们必须明确普通模块(即 NormalModule 实例)与属于 ModuleGraph 的模块之间的区别。在 ModuleGraph 中的节点被称为 ModuleGraphModule,它实际上就是一个增强了的 NormalModule 实例。ModuleGraph 使用一个映射来跟踪这些增强的模块,映射的类型为:Map<Module, ModuleGraphModule>。强调这些细节至关重要,因为如果只有 NormalModule 实例,能做的操作相对有限——它们本身并不具备相互通信的能力。ModuleGraph 赋予了这些原生模块更多的信息,通过前述的映射关系,实现模块间的相互连接,将每个 NormalModule 与一个 ModuleGraphModule 关联起来。在“构建 ModuleGraph”一节的最后部分,当我们使用 ModuleGraph 及其内部映射来遍历模块图时,这将变得更加清晰。我们将属于 ModuleGraph 的模块简单地称为模块,因为两者之间的区别仅仅是一些额外的属性。

对于 ModuleGraph 的节点,有几个明确定义的属性:传入连接和传出连接。在 ModuleGraph 里,连接本身也是一个重要的实体,它包含来源模块、目标模块以及连接这两个模块的依赖等重要信息。具体来说,依照前面的图示,我们可以看到一个新的连接被建立:

// 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——汇聚其关键信息。这是因为所有这些模块树最终都将与 null 模块(ModuleGraph 的根模块)相连。与 null 模块的连接是通过 EntryDependency 和从入口文件创建的模块建立的。这就是我理解的 ModuleGraph

[译]深度解析Webpack打包过程(2万字预警)

构建 ModuleGraph

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

创建的第一个模块

为了确保没有任何不确定性,NormalModule 只是文件源代码的反序列化版本,它仅仅是一个原始字符串,没有太多价值,因此 Webpack 无法做太多事情。NormalModule 也会将源代码存储为字符串,但同时也包含其他有意义的信息和功能,例如:应用于它的 loader、构建模块的逻辑、生成运行时代码的逻辑、它的哈希值等等。换句话说,从 Webpack 的角度来看,NormalModule 是一个简单的原始文件的有用版本。

[译]深度解析Webpack打包过程(2万字预警)

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

你可以看到,null 模块与从 entry 对象的每一项生成的模块树的根模块都有一个连接。图中的每条边代表了两个模块之间的连接,每个连接都包含了关于源节点、目标节点和依赖关系的信息(这些信息解释了为何这两个模块会相连的问题)。

现在我们对 ModuleGraph 有了更多的了解,让我们来看看它是如何构建的:

构建 ModuleGraph

正如我们在前一节中看到的,ModuleGraph 从一个 null 模块开始,其直接后代是从 entry 对象条目构建起的各个模块树的根模块。因此,为了理解 ModuleGraph 是如何构建的,我们将逐步分析单个模块树的构建流程。

首个被创建的模块

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

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

如第一节所提到的,最终我们将会得到一个请求为 './a.js' 的 EntryDependency。这个 EntryDependency 通过一个模块工厂——NormalModuleFactory——提供了一种从该请求中创建模块的方式。这就是我们第一节所讨论的内容。

接下来,NormalModuleFactory 开始发挥作用。NormalModuleFactory 在成功执行后,它将创建一个 NormalModule

为了消除可能的疑惑,请理解 NormalModule 本质上就是对文件源代码进行解析,文件源代码不过就是一个原始字符串。原始字符串本身并没有太大价值,因此 Webpack 不能直接使用它。NormalModule 会将源代码以字符串形式存储,与此同时,它还包含其他重要的信息和功能,比如:应用于模块的 loader、构建模块的逻辑、生成运行时代码的逻辑、哈希值以及更多内容。换句话说,从 Webpack 的视角来看,NormalModule 是原始文件的升级版本。

NormalModuleFactory 在生成 NormalModule 前需要执行若干步骤。模块创建完毕后,还有一些工作要做,比如构建模块以及处理它的依赖(如果存在的话)。

下面是我们一直在使用的示例图,现在我们将专注于构建 ModuleGraph 的部分:

[译]深度解析Webpack打包过程(2万字预警)

NormalModuleFactory 通过调用其 create 方法启动它的构建流程。随后,解析过程就开始了。在这个阶段,会解析 request(文件的路径)以及该类型文件所需的 loader。需要注意的是,这一步只是确定 loader 的文件路径,并没有实际调用 loader。

模块的构建过程

在所有必要的文件路径解析完成后,就会创建 NormalModule。然而,此时的模块还没有太大价值。大量信息在模块构建后产生。NormalModule 的构建过程包括下面几个步骤:

  1. 首先,loader 将对原始代码进行处理;如果存在多个 loader,则一个 loader 的输出可能成为另一个 loader 的输入(配置文件中 loader 的顺序很重要);
  2. 其次,经过所有 loader 处理后得到的字符串将被 Acorn(一个 JavaScript 解析器)解析,从而生成给定文件的抽象语法树(AST);
  3. 最后,将对 AST 进行分析;这一分析工作是必要的,因为在这一阶段将确定当前模块的依赖关系(例如其他模块),Webpack 能够检测一些特别的函数(如 require.contextmodule.hot 等);AST 的分析工作在 JavascriptParser 中进行;这部分过程是最重要的过程之一,因为后续的打包过程中的许多内容都依赖于这部分;

通过生成的AST进行依赖分析

一种思考分析过程的方式,不需要过多细节,可以是这样的:

[译]深度解析Webpack打包过程(2万字预警)

moduleInstance 指的是从 index.js 文件创建的 NormalModule。红色的 dep 指的是从第一个 import 语句创建的依赖项,蓝色的 dep 指的是第二个 import 语句。请注意,这是对实际情况的一种简化。实际上,正如之前提到的,依赖是在分析 AST 后才被添加的。

在分析完抽象语法树(AST)之后,我们将继续进行本节开头所讨论的模块树构建过程。下一步是处理在前一步骤中发现的依赖。按照上面的示例图,index 模块有两个依赖,它们也是模块,即 math.jsutils.js。但在依赖变成实际模块之前,我们只有 index 模块,其 module.dependencies 有两个值,这些值包含了诸如模块 request(文件路径)、导入指令(例如 sum, greet)等信息。为了将它们转变成模块,我们需要使用这些依赖项映射到的 ModuleFactory,并遵循前文描述的步骤(这一循环过程由示意图首部的虚线箭头指示)。在处理完当前模块的依赖之后,这些依赖可能也有自己的依赖,这个过程一直进行,直到没有更多的依赖为止。这就是模块树的构建方式,当然,同时确保正确建立父模块和子模块之间的连接。

根据我们迄今为止掌握的知识,亲自实践 ModuleGraph 会是一个很好的练习。为此,我们可以实现一个自定义插件,这个插件将遍历 ModuleGraph。下面提供了一张示意图,展示各模块间的依赖关系:

[译]深度解析Webpack打包过程(2万字预警)

为确保图中的内容能被清晰理解,a.js 文件导入了 b.js 文件,而 b.js 又同时导入了 b1.jsc.js 文件。接着,c.js 文件导入了 c1.jd.js 文件,最终,d.js 文件导入了 d1.js 文件。最后,ROOT 指的是 null module,它是 ModuleGraph 的根。entry 只包含一个值,即 a.js

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

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

// 我们通过使用 `tap` 方法向现有 webpack 钩子(hooks)中添加逻辑的方式,它具有如下签名:
// `tap(string, callback)`
// 其中 `string` 主要用于调试目的,表明自定义逻辑是从哪里添加的。
// `callback` 的参数取决于我们要向其添加自定义功能的钩子。

class UnderstandingModuleGraphPlugin {
    apply(compiler) {
        const className = this.constructor.name;
        // 关于 `compilation` 对象:它是存放打包过程中的大部分*状态*的地方。
        // 其中包含了诸如模块图(module graph)、chunk 图、创建的 chunk、
        // 创建的模块(modules)、生成的资源(assets)等信息。
        compiler.hooks.compilation.tap(className, (compilation) => {
            // `finishModules` 的调用发生在*所有*模块构建完成之后。
            // 包括它们的依赖,依赖的依赖等等。
            compilation.hooks.finishModules.tap(className, (modules) => {
                // `modules` 是一个 set,包含所有已构建模块。
                // 这些都是 `NormalModule` 实例。
                // 再次强调,`NormalModule` 是由 `NormalModuleFactory` 生成的。
                // console.log(modules);

                // 检索 **模块映射**(Map<Module, ModuleGraphModule>)。
                // 它包含了我们遍历模块图所需要的全部信息。
                const {
                    moduleGraph: { _moduleMap: moduleMap },
                } = compilation;

                // 让我们以深度优先搜索(DFS)的方式遍历模块图。
                const dfs = () => {
                    // 请注意,`ModuleGraph` 的根模块是*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",
                        );

                        // 获取关联的 `ModuleGraphModule`,其具有额外的属性,
                        // 除了 `NormalModule` 的属性之外,
                        // 我们还可以使用这些属性来进一步遍历模块图。
                        const correspondingGraphModule = moduleMap.get(crtNode);

                        // `Connection` 的 `originModule` 是箭头的起点
                        // 而 `Connection` 的 `module` 是箭头的终点。
                        // 因此,`Connection` 的 `module` 是子节点。
                        // 你可以在这里,了解更多关于模块图的连接信息:
                        // https://github.com/webpack/webpack/blob/main/lib/ModuleGraphConnection.js#L53。
                        // `correspondingGraphModule.outgoingConnections` 要么是一个 Set,要么是 undefined(如果节点没有子节点的情况下)。
                        // 我们使用 `new Set` 是因为一个模块可能有多个连接引用同一个模块。
                        // 例如,一个 `import foo from 'file.js'` 的结果是两个连接:一个是简单的 import,
                        // 另一个是 `foo` 默认指定符。这是一个实现细节,你不需要为此担心。
                        const children = new Set(
                            Array.from(
                                correspondingGraphModule.outgoingConnections ||
                                    [],
                                (c) => c.module,
                            ),
                        );
                        for (const c of children) {
                            traverse(c);
                        }
                    };

                    // 开始遍历。
                    traverse(root);
                };

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

根据模块层次结构,在插件运行后,我们会得到以下输出:

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

现在 ModuleGraph 已经构建完成,希望你已经掌握它了,那么我们就可以继续探索下一阶段的内容了。根据示意图,下一步将创建 chunks,让我们来深入了解一下。但在此之前,有必要先明确一些重要概念,比如 ChunkChunkGroupEntryPoint

明确 ChunkChunkGroupEntryPoint 的含义

在对模块有了深入的了解之后,我们将在此基础上解释本节标题中提到的概念。简单回顾一下,模块可以被看作是文件的升级版本。模块一旦被创建和构建,它就包含了许多有意义的信息,除了原始代码,还包括:使用的 loader、它的依赖关系、它的导出(如果有的话)、它的哈希等等。

一个 Chunk 封装了一个或多个模块。乍一看,人们可能会认为入口文件的数量(一个入口文件 = entry 对象的一个项)会直接决定生成的 Chunk 数量。这个观点是部分正确的,因为实际上,即使 entry 对象只包含一个条目,最终生成的 Chunk 数量也可能超出一个。的确,每个 entry 条目都会在 dist 目录中生成一个对应的 Chunk,但也有可能会隐式创建额外的 Chunk,例如在使用 import() 函数的场景下。但不管 Chunk 是如何被创建的,每个 Chunk 都会在 dist 目录中对应一个文件。在后续构建 ChunkGraph 的环节中,我们会详细解释哪些模块会被包含在 Chunk 之中,以及哪些不会。

一个 ChunkGroup 包含一个或多个 chunk。ChunkGroup 可以是另一个 ChunkGroup 的父级或子级。例如,当使用动态导入时,对于每个使用 import() 函数的导入,都会创建一个 ChunkGroup,其父级将是现有的 ChunkGroup,即包含使用 import() 函数的文件(即模块)的 ChunkGroup

一个 EntryPoint 是一种 ChunkGroup 类型,entry 对象中的每一项都会创建一个。

构建 ChunkGraph

让我们回顾一下,到目前为止我们所构建的是 ModuleGraph,正如我们在前面章节中讨论的那样。然而,ModuleGraph 仅是打包过程中众多关键部分之一。只有充分利用它,我们才能实现代码分割等高级功能。

在打包过程的这一阶段,对于来自 entry 对象的每个项,都将有一个 EntryPoint。由于它是 ChunkGroup 类型,因此它将至少包含一个 chunk。因此,如果入口对象有3个项,则将有3个 EntryPoint 实例,每个实例都有一个 chunk,也称为 entrypoint chunk,其名称是 entry 项键的值。与入口文件相关联的模块称为入口模块,每个模块都将属于其 entrypoint chunk。它们很重要,因为它们是 ChunkGraph 构建过程的起点。请注意,一个 chunk 可以包含多个 entry 模块:

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

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

在深入具体细节之前,让我们以一个例子来探讨构建过程:

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

这个例子将包含之前提到的内容:ChunkGroups(包括动态导入)的父子关系,chunks 以及 EntryPoints

ChunkGraph 是以递归的方式构建的。它首先将所有入口模块添加到队列中。然后,当处理入口模块时,也就是要检查其依赖(也是模块),并将每个依赖添加到队列中。这样一直重复,直到队列变为空为止。这个过程的一部分是访问模块。然而,这只是第一部分。回想一下,ChunkGroup 可以是其他 ChunkGroup 的父级/子级。这样的关联关系会在第二阶段中处理。例如,如先前所述,动态导入(即 import() 函数)将导致新的子 ChunkGroup。在 Webpack 中,import() 表达式定义了一个异步的依赖。在 import('./foo.js'.then(module => ...) 的情况下,很明显我们的意图是异步加载某些内容,并且很明显,在使用模块之前,必须解决 foo 的所有依赖(即模块),包括 foo 本身。我们将在以后的文章中详细讨论 import() 函数的工作原理,包括它的一些特殊特性(例如魔术注释和其他选项)。

现在,让我们看一下由上面的配置创建的 ChunkGraph 的示意图:

[译]深度解析Webpack打包过程(2万字预警)

该图展示了一个非常简化的 ChunkGraph,但它足以展示 chunk 与 ChunkGroup 之间的关系。我们可以看到有4个 chunk,因此将会有4个输出文件。foo chunk 包含4个模块,其中2个是入口模块。bar chunk 只有1个入口模块,其余的均为普通模块。我们还可以注意到,每一个 import() 表达式都会生成一个新的 ChunkGroup(其父节点是 bar EntryPoint),进而生成一个新的 chunk。

产生的文件内容是基于 ChunkGraph 确定的,因此这就是为什么它对整个打包过程非常重要的原因。我们将在下一节简要介绍 chunk asset(即产生的文件)。

在使用 ChunkGraph 之前,有必要提及一些其特有的属性。与 ModuleGraph 类似,ChunkGraph 的节点被称为ChunkGraphChunk,它仅仅是具有额外属性的 chunk,比如这个 chunk 包含的模块、入口模块以及其他属性。就像 ModuleGraph 一样,ChunkGraph 借助于一个 WeakMap<Chunk, ChunkGraphChunk> 签名的映射来跟踪带有额外属性的 chunk。与 ModuleGraph 的映射相比,ChunkGraph 维护的映射不包含 chunk 之间连接的信息。相反,所有必要的信息(比如它所属的 ChunkGroups)都保存在 chunk 内部。请记住,chunk 是被组织在 ChunkGroup 中的,而这些 chunk group 之间可能存在父子关系(就像我们在上面的图表中看到的那样)。模块虽然相互依赖,但它们并没有严格的父模块概念。

现在让我们来尝试在自定义插件中使用 ChunkGraph,以便深入了解其工作原理。请注意,我们将沿用前文示意图中所展示的场景作为例:

const path = require("path");

// 我们以这种方式打印是为了突出显示 `ChunkGroup` 之间的父子关系。
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) => {
      // `afterChunks` 钩子是在构建 `ChunkGraph` 之后调用的。
      compilation.hooks.afterChunks.tap(className, (chunks) => {
        // `chunks` 是所有创建的 chunk 的集合。chunk 是根据创建顺序添加到这个集合中的。
        // console.log(chunks);

        // 正如我们在本文前面所说的,`compilation` 对象包含绑定过程的状态。
        // 在这里,我们还可以找到已创建的所有 `ChunkGroup`(包括 `Entrypoint` 实例)。
        // console.log(compilation.chunkGroups);

        // `EntryPoint` 是为 `entry` 对象中的每个项目创建的 `ChunkGroup` 类型。
        // 在我们当前的示例中,有2个。因此,为了遍历 `ChunkGraph`,
        // 我们必须从存储在 `compilation` 对象中的 `EntryPoints` 开始。
        // 有关 `entrypoints` map<string,Entrypoint> 的详细信息:
        const { entrypoints } = compilation;

        // 更多关于 `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` - 一个 `ChunkGroup` 可以包含一个或多个 chunk。
          const allModulesInChunkGroup = chunkGroup.chunks
            .flatMap((c) => {
              // 使用 `ChunkGraph` 中存储的信息来获取 chunk 中包含的模块。
              const associatedGraphChunk = chunkMap.get(c);

              // 这也包括*入口模块*。
              // 使用扩展运算符,因为 `.modules` 在这种情况下是一个 Set。
              return [...associatedGraphChunk.modules];
            })
            // 模块的 resource 是绝对路径,我们只关注模块的文件名。
            .map((module) => path.basename(module.resource));
          printWithLeftPadding(
            `The modules that belong to this chunk group: ${allModulesInChunkGroup.join(", ")}`,
            paddingLength,
          );

          console.log("\n");

          // 一个 `ChunkGroup` 可以拥有子 `ChunkGroup`。
          [...chunkGroup._children].forEach((childChunkGroup) =>
            printChunkGroupsInformation(childChunkGroup, paddingLength + 3),
          );
        };

        // 以深度优先搜索(DFS)的方式遍历 `ChunkGraph`。
        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 资源

需要注意的是,生成的文件不只是原始文件的原样复制,为了实现其功能,Webpack 需要加入一些自定义代码来确保整个系统能够正常工作。

这就引出了一个问题:Webpack 如何决定要生成什么代码。这一切都从最基础(也是最有用)的层次开始:模块。模块可以导出成员、导入其他成员、使用动态导入、使用 Webpack 特定的函数(比如 require.resolve)等等。根据模块的源代码,Webpack 可以确定为了实现期望功能需要生成哪些代码。这一过程始于 AST 分析阶段,即发现依赖的阶段。尽管到目前为止我们一直在使用依赖和模块,但实际情况略微复杂。

例如,一个简单的 import { aFunction } from './foo' 将导致两个依赖(一个是对 import 语句本身,另一个是导入的成员,即 aFunction),这两个依赖项将创建出一个模块。另一个例子是 import() 函数。正如之前的章节中提到的,这将导致一个异步的依赖块,其中一个依赖是动态导入特有的 ImportDependency

这些依赖至关重要,因为它们提供了应该生成什么代码的线索。例如,ImportDependency 精确地告诉 Webpack 应该异步模块并使用其导出的成员。这些线索可以被称为运行时需求。例如,如果模块导出了一些成员,将会有一些依赖,即 HarmonyExportSpecifierDependency,它将告知 Webpack 需要处理导出成员的逻辑。

总之,一个模块会带有它的运行时需求,这取决于模块在其源代码中是被如何使用的。一个 chunk 的运行时需求将是其所有属于该 chunk 的模块的所有运行时需求的集合。现在 Webpack 了解了 chunk 的所有需求,它将能够适当地生成运行时代码。

这个过程也被称为渲染过程(rendering process),我们将在专门的文章中详细讨论它。目前,重要的是理解渲染过程高度依赖于 ChunkGraph,因为它包含了 chunk 的集合(即 ChunkGroupEntryPoint),其中包含了 chunks,chunks 中包含了模块,而模块以细粒度方式包含了有关 Webpack 将生成的运行时代码的信息和提示。

最后

为了帮助你全面而精准地理解 webpack,本文尽量避免不必要的细节,力求从多个角度进行讲解。Webpack 虽然功能丰富且深奥,但本文的目标是简化这些概念,让它们变得更加容易理解和掌握。

感谢阅读!

原文链接:https://juejin.cn/post/7313423853223985167 作者:聪聪正在递归下降

(0)
上一篇 2023年12月18日 上午10:11
下一篇 2023年12月18日 上午10:23

相关推荐

发表回复

登录后才能评论