webpack 4 源码主流程分析(八):生成 chunk

this._addModuleChain 的回调里,得到了生成的入口 module。触发 compilation.hooks:succeedEntry 后,执行 return callback(null, module),回到文件 Compile.jscompilemake 钩子的回调里:

1
2
3
4
5
6
7
8
9
10
11
12
13
this.hooks.make.callAsync(compilation, (err) => {
//...
compilation.finish((err) => {
//...
compilation.seal((err) => {
//...
this.hooks.afterCompile.callAsync(compilation, (err) => {
//...
return callback(null, compilation);
});
});
});
});

compilation.finish

执行 compilation.finish,触发 compilation.hooksfinishModules,执行插件 FlagDependencyExportsPlugin 注册的事件,作用是遍历所有 moduleexport 出来的变量以数组的形式,单独存储到 module.buildMeta.providedExports变量下。

然后执行 reportDependencyErrorsAndWarnings 收集生成每一个 module 时暴露出来的 errwarning

最后走回调执行 compilation.seal

compilation.seal

compilation.seal 里触发了海量 hooks,为我们侵入 webpack 构建流程提供了海量钩子。先执行(我们先略过没有注册方法的钩子):

1
this.hooks.seal.call();

触发插件 WarnCaseSensitiveModulesPlugin:模块文件路径需要区分大小写的警告

1
this.hooks.optimizeDependencies.call(this.modules);

production 模式会触发插件:

  • SideEffectsFlagPlugin:识别 package.json 或者 module.rulessideEffects 标志(纯的 ES2015 模块),安全地删除未用到的 export 导出
  • FlagDependencyUsagePlugin:编译时标记依赖 unused harmony export 用于 Tree shaking

chunk 初始化

在触发 compilation.hooks:beforeChunks 后,开始遍历入口对象 this._preparedEntrypoints,为每一个入口生成一个 chunk

1
const chunk = this.addChunk(name);

该方法里做了缓存判断后执行 new Chunk(name),并同时添加 chunkCompilation.chunks,继续执行:

1
const entrypoint = new Entrypoint(name);

Entrypoint 类扩展于 ChunkGroup 类,是 chunks 的集合,主要用来优化 chunk graph

继续执行设置了 Compilation.runtimeChunk & Compilation.namedChunkGroups & Compilation.entrypoints & Compilation.chunkGroupsChunkGroup.origins,然后执行:

1
2
GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
GraphHelpers.connectChunkAndModule(chunk, module);

建立了 chunkentrypointchunkmodule 之间的联系,然后执行:

1
this.assignDepth(module);

根据各个模块依赖的深度(多次依赖取最小值)设置 module.depth,入口模块则为 depth = 0

遍历完 this._preparedEntrypoints 后,然后执行:

生成 chunk graph

1
buildChunkGraph(this, /** @type {Entrypoint[]} */ (this.chunkGroups.slice()));

buildChunkGraph 用于生成并优化 chunk 依赖图,建立起各模块之前的关系。分为三阶段:

1
2
3
4
5
6
7
8
9
10
11
// PART ONE

visitModules(compilation, inputChunkGroups, chunkGroupInfoMap, chunkDependencies, blocksWithNestedBlocks, allCreatedChunkGroups);

// PART TWO

connectChunkGroups(blocksWithNestedBlocks, chunkDependencies, chunkGroupInfoMap);

// Cleaup work

cleanupUnconnectedGroups(compilation, allCreatedChunkGroups);

第一阶段

先执行:

1
const blockInfoMap = extraceBlockInfoMap(compilation);

得到一个 map 结构: module 与该 module 内导入其他模块的关系,同步存入 modules,异步存入 blocks。以 demo 为例,得到 blockInfoMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
//...map结构
0:{
key:NormalModule, //a
value:{
blocks:[ImportDependenciesBlock],//异步
modules:[NormalModule] //b modules为set结构
}
},
1:{
key: ImportDependenciesBlock,
value:{
blocks: [],
modules:[NormalModule] //c
}
}
2:{
key: NormalModule, //c
value:{
blocks: [ImportDependenciesBlock],
modules:[NormalModule] //d
}
}
//........
}

继续执行,设置了 queue 数组,push 入口 module 和对应的 action 等信息组成的对象,用于 while 循环;设置了 chunkGroupInfoMap,他映射了 chunkGroup 和与他相关的信息对象,然后执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
while (queue.length) {
//...
while (queue.length) {
//...
if (chunkGroup !== queueItem.chunkGroup) {
// 重置更新chunkGroup
}
switch (queueItem.action) {
case ADD_AND_ENTER_MODULE: {
// 建立chunk和module之间的联系
}
case ENTER_MODULE: {
// 设置 chunkGroup._moduleIndices 和 module.index,然后 queue.push 一个新的该 module 的 queueItem,action 设为 LEAVE_MODULE
}
case PROCESS_BLOCK: {
// 1. 遍历 blockInfoMap 里的同步模块 modules,如果对应 chunk 已有此模块则跳过,如果 minAvailableModules 有此模块则一个新的 queueItem 存入 skippedItems 数组,没有则该 queueItem 存入 queue,其中 queue 的 action 都设为 ADD_AND_ENTER_MODULE
// 2. 遍历 blockInfoMap 里的异步模块 blocks
// 2.1 创建一个对应import依赖的chunkGroup和chunk,并建立两者的联系,然后更新了 compilation.chunkGroups 和 compilation.namedChunkGroups,chunkGroupCounters(计数 map),blockChunkGroups(映射依赖和 ChunkGroup的关系 map),allCreatedChunkGroups(收集被创建的ChunkGroup set)
// 2.2 更新 chunkDependencies(map) 建立前一个 ChunkGroup 与新的 ChunkGroup 和 import 依赖的映射
// 2.3 更新 queueConnect(map) 建立前一个 ChunkGroup 与新的 ChunkGroup 的映射
// 2.4 更新 queueDelayed,同 queue,注意 module 是前一个的 module
}
case LEAVE_MODULE: {
// 设置 chunkGroup._moduleIndices2 和 module.inde2
}
}
}
while (queueConnect.size > 0) {
// 1. 在 chunkGroupInfoMap 中设置前一个 ChunkGroup 的信息对象的 resultingAvailableModules, children
// 2. 在 chunkGroupInfoMap 中初始化新的 ChunkGroup 与他相关的信息对象的映射并设置了 availableModulesToBeMerged
if (outdatedChunkGroupInfo.size > 0) {
// 1.获取设置新的 ChunkGroup 信息对象的 minAvailableModules
// 2.如果新的 ChunkGroup 信息对象的 skippedItems 不为空则 push 到 queue
// 3.如果新的 ChunkGroup 信息对象的 children 不为空,则更新 queueConnect 递归循环
}
}
// 把queueDelayed 放入queue走while的最外层循环,目的的同步循环处理完后,然后才处理异步module
if (queue.length === 0) {
const tempQueue = queue;
queue = queueDelayed.reverse();
queueDelayed = tempQueue;
}
}
  • 在内部 whilequeue.length 循环里( while+push 防递归爆栈,后序深度优先),从入口 module 开始,解析了所有同步 module 并建立了 modulechunk 的联系;解析了所有第一层异步的 module,并为每个不同 mudule 都新建了 chunkGroupchunk 并建立了两者的联系。
  • 然后在 whilequeueConnect.size 的循环里,更新了 chunkGroupInfoMap 中前一个 ChunkGroup 的信息对象和初始化了新的 ChunkGroup 的信息对象,并获取了最小可用模块。
  • 同步模块循环处理结束后,开始处理异步 module,将 queueDelayed 赋给 queue,走外部 whilequeue.length 的循环。
  • 处理异步模块的时候,queue 里的 blockImportDependenciesBlock 依赖,然后更新 chunkGroup 后, switchPROCESS_BLOCK 获得本次异步对应的真正模块,后面的处理数据都将在新的 ChunkGroup 信息对象上。就这样循环处理,最终得到一个 Map 结构的 chunkGroupInfoMap。以本 demo 为例,得到:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{
//...map结构
0:{
key:Entrypoint, //groupDebugId:5000
value:{
availableModulesToBeMerged:Array(0)
children:Set(1) {} //ChunkGroup 5001
chunkGroup:Entrypoint
minAvailableModules:Set(0)
minAvailableModulesOwned:true
resultingAvailableModules:Set(3)
skippedItems:Array(0)
}
},
1:{
key: ChunkGroup, //groupDebugId:5001
value:{
availableModulesToBeMerged:Array(0)
children:Set(1) {} //ChunkGroup 5002
chunkGroup:Entrypoint
minAvailableModules:Set(3)
minAvailableModulesOwned:true
resultingAvailableModules:Set(5)
skippedItems:Array(0)
}
}
2:{
key: ChunkGroup, //groupDebugId:5002
value:{
availableModulesToBeMerged:Array(0)
children:undefined
chunkGroup:Entrypoint
minAvailableModules:Set(5)
minAvailableModulesOwned:true
resultingAvailableModules:undefined
skippedItems:Array(1)
}
}
}

第二阶段

遍历 chunkDependencieschunkDependenciesMap 结构,保存着前一个 ChunkGroup 与新的 ChunkGroupimport 依赖之间的映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
//...map结构
0:{
key:Entrypoint, //groupDebugId:5000
value:[
{
block:ImportDependenciesBlock,
chunkGroup:ChunkGroup //groupDebugId:5001
}
]
},
1:{
key:ChunkGroup, //groupDebugId:5001
value:[
{
block:ImportDependenciesBlock,
chunkGroup:ChunkGroup //groupDebugId:5002
}
]
},
}

在判断如果前一个 ChunkGroup 信息对象的可用模块 resultingAvailableModules 包含后一个 ChunkGroup.chunks[]._modules,则分别建立 import 依赖与对应的 ChunkGroup,前一个 chunkGroup 和后一个 chunkGroup 的关系:

1
2
3
GraphHelpers.connectDependenciesBlockAndChunkGroup(depBlock, depChunkGroup); // ImportDependenciesBlock与chunkGroup建立联系

GraphHelpers.connectChunkGroupParentAndChild(chunkGroup, depChunkGroup); // chunkGroup之间建立联系:_children和_parents

第三阶段

遍历 allCreatedChunkGroupsallCreatedChunkGroups 即为异步被创建的 ChunkGroup,判断 chunkGroup 有没有父的 chunkGroup_parents),如果没有执行:

1
2
3
4
5
6
for (const chunk of chunkGroup.chunks) {
const idx = compilation.chunks.indexOf(chunk);
if (idx >= 0) compilation.chunks.splice(idx, 1);
chunk.remove('unconnected');
}
chunkGroup.remove('unconnected');

即解除 module,chunkGroup,chunk 三者之间的联系。

最终每个 module 与每个 chunk,每个 chunkGroup 和他们之间都建立了联系,优化形成了 chunk Graph

seal 里继续执行,先将 compilation.modulesindex 属性大小排序,然后执行:

1
this.hooks.afterChunks.call(this.chunks);

触发插件 WebAssemblyModulesPlugin:设置与 webassembly 相关的报错信息,到此 chunk 生成结束。

本章小结

  1. finish 回调中执行的 seal 方法里,包含了海量钩子用于我们侵入 webpack 的封包阶段;
  2. 在遍历入口文件实例化生成 chunk 时,同时实例化了 Entrypoint 等,并建立了入口 modulechunkEntrypoint 之间的联系;
  3. 通过 buildChunkGraph 的三个阶段,让所有的 module、chunk、chunkGroup 之间都建立了联系,形成了 chunk Graph
  4. 最后触发钩子 afterChunks 标志这 chunk 生成结束。
---- 本文结束,感谢您的阅读 ----