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 生成的运行时代码的信息和提示。