webpack初步理解
Webpack 是一个非常强大且有趣的工具,被视为当今许多 Web 开发人员用来构建其应用程序的技术中的基本组件。然而,许多人会认为使用它是一个
webpack bundle过程图表

modules
模块是一个文件的升级版本。一个模块,一旦创建并且构建之后,除了包含原始的文件代码之外,还有一些其他信息,比如模块使用的加载器,模块的依赖项,模块的导出以及模块的hash值等等其他。
The entry
object
对entry对象(也可以称为入口对象)记住一点:入口对象中的每一项都可以被认为是模块树中的根模块。模块树,因为根模块可能需要一些其他模块(可以称为依赖项),这些模块(依赖项也是模块)可能需要其他模块等等,因此您可以在更高的级别上看到如何构建这样的树。所有这些模块树都存储在一个 ModuleGraph(模块图) 中。
另外一点我们需要知道的是:webpack 是建立在许多插件之上的。尽管bundle过程已经很好地建立起来,但是可以插入很多方法来添加自定义逻辑。webpack的扩展性是通过hooks来实现的。比如,你可以在模块图构建好之后,当新的chunk生成资产的时候以及模块将要构建之前(运行加载器或者解析源代码时),添加自己的一些自定义逻辑。hooks非常有趣,可以为许多与 webpack 定制相关的问题提供解决方案。大多数时候,hooks是根据它们的用途分组的,每一个插件都有明确定义的用途。例如,有一个插件负责处理 import() 函数(负责解析注释和参数)它被称为 ImportParserPlugin,它所做的只是在 AST 解析期间遇到 import() 调用时添加一个hook。
同时毫无疑问的是,有几个负责处理入口对象的插件。有一个 EntryOptionPlugin,它实际上接受入口对象为参数并为入口对象中的每个项目创建一个 EntryPlugin。这部分很重要:入口对象的每一项都将产生一棵模块树(所有这些树都是相互分离的)。基本上,EntryPlugin 开始创建一个模块树,每个模块树都会将信息添加到同一个地方,即 ModuleGraph。通俗地说,我们会说 EntryPlugin 启动了这个复杂的过程。

结合初始图片,值得一提的是,EntryPlugin 也是创建 EntryDependency 的地方。基于上图,让我们通过自己实现EntryOptionsPlugin来进一步了解EntryOptionsPlugin的重要性:
lass 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);
};
});
}
};
在本节的最后一部分,我们将稍微扩展一下依赖项是什么,因为我们将在本文中进一步使用它。您现在可能想知道 EntryDependency 是什么以及为什么需要它。从我的角度来看,在创建新模块时,这一切都归结为一种聪明的抽象。简单地说,依赖只是实际模块实例的初级阶段。例如,即使 entry 对象的项目再webpack 的视角来看也是依赖项,它们指出要创建的模块实例的最低要求:它的路径(例如 ./a.js、./b.js)。没有依赖项就无法创建模块,因为依赖项包含模块的请求以及其他重要信息,即可以找到模块源的文件的路径(例如'./a.js')。依赖项还指示如何构造该模块,它怎样使用一个模块工厂来完成模块构建。模块工厂知道如何从原始状态(字符串源代码)转化到一些可由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对象可能只有一个项目并且结果块的数量可能大于一个。确实,对于每个条目项,在 dist 目录中都会有一个相应的块,但是可以隐式创建其他块,例如在使用 import() 函数时。但是不管是怎么创建的,每个chunk都会在dist目录下有一个对应的文件。我们将在构建 ChunkGraph 部分对此进行扩展,我们将阐明哪些模块属于一个块,哪些不属于。
一个 ChunkGroup 包含一个或多个块。一个 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 生成的运行时代码的信息和提示。
本节标志着本文理论部分的结束。
版权声明:著作权归作者所有。