接上文:浅析 webpack 打包流程(原理) 二 - 递归构建 module
五、生成 chunk
生成 chunk 阶段概述:在
compilation.finish
回调中执行的 seal 方法中,触发海量钩子,就此侵入 webpack 的封包阶段;
1.首先对所有import
和export
做标记,以实现最后构建资源阶段的 treeshaking;
2.遍历入口文件为每个入口生成初始 chunk 的同时,也实例化了 EntryPoint(继承自 ChunkGroup 类),并建立了入口 module 和 chunk、entryPoint 之间的联系;
3.通过 buildChunkGraph 的三个阶段,生成异步 chunk 和 包含它的chunkGroup,将所有 module、chunk、chunkGroup 都建立起关联,形成了 chunkGraph。
4.最后将compilation.modules
排序,再触发afterChunks 钩子
,chunk 生成结束。
这部分都是 webpack 的预处理 和 chunks 默认规则的实现,后面 chunk 优化阶段会暴露很多钩子,webpack 会根据我们配置的插件来进行优化。
上一步我们 addEntry 方法 this._addModuleChain 的传的回调里return callback(null, module);
,回到compile
方法的 compiler.hooks.make.callAsync()
,执行它的回调:
// /lib/Compiler.js
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
compilation.finish(err => {
if (err) return callback(err);
compilation.seal(err => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
此时compilation.modules
已经有了所有的模块:a、c、b、d
。
执行compilation.finish
方法,触发compilation.hooks:finishModules
,执行插件 FlagDependencyExportsPlugin 注册的事件,作用是遍历所有 module,将 export 出来的变量以数组的形式,单独存储到 module.buildMeta.providedExports 变量下。
然后通过遍历为每一个 module 执行compilation.reportDependencyErrorsAndWarnings
,收集生成它们时暴露出来的 err 和 warning。
最后走回调执行compilation.seal
,提供了海量让我们侵入 webpack 构建流程的 hooks。seal 字面意思是封包,也就是开始对上一步生成的 module 结果进行封装。
先执行 (我们先略过没有注册方法的钩子)this.hooks.seal.call();
,触发插件 WarnCaseSensitiveModulesPlugin:在 compilation.warnings 添加 模块文件路径需要区分大小写的警告。
再是this.hooks.optimizeDependencies.call(this.modules)
,production 模式会触发插件:
SideEffectsFlagPlugin
:识别 package.json 或者 module.rules 的 sideEffects 标记的纯 ES2015 模块(纯函数),安全地删除未用到的 export 导出;FlagDependencyUsagePlugin
:编译时标记依赖unused harmony export
,用于 Tree shaking
5.1 chunk 初始化
在触发compilation.hooks:beforeChunks
后,开始遍历入口对象 this._preparedEntrypoints,每个入口 module 都会通过addChunk
去创建一个空 chunk(并添加到compilation.chunks
),此时不包含任何与之相关联的 module。之后实例化一个 EntryPoint,把它添加到compilation.chunkGroups
中。接下来调用 GraphHelpers 模块提供的方法来建立起 chunkGroup 和 chunk 之间的联系,以及 chunk 和 入口 module 之间的联系(这里还未涉及到入口依赖的 module):
// /lib/Compilation.js
for (const preparedEntrypoint of this._preparedEntrypoints) {
const module = preparedEntrypoint.module;
const name = preparedEntrypoint.name;
// addChunk 方法进行缓存判断后执行 new Chunk(name),并同时添加 chunk 到 compilation.chunks
const chunk = this.addChunk(name);
// Entrypoint 类扩展于 ChunkGroup 类,是 chunks 的集合,主要用来优化 chunk graph
const entrypoint = new Entrypoint(name); // 每一个 entryPoint 就是一个 chunkGroup
entrypoint.setRuntimeChunk(chunk); // 设置 runtimeChunk,就是运行时 chunk
entrypoint.addOrigin(null, name, preparedEntrypoint.request);
this.namedChunkGroups.set(name, entrypoint);
this.entrypoints.set(name, entrypoint);
this.chunkGroups.push(entrypoint); // 把 entryPoint 添加到 chunkGroups
// 建立 chunkGroup 和 chunk 之间的联系:
GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
// 建立 chunk 和 入口 module 之间的联系(这里还未涉及到入口的依赖模块)
GraphHelpers.connectChunkAndModule(chunk, module);
chunk.entryModule = module;
chunk.name = name;
// 根据各个模块依赖的深度(多次依赖取最小值)设置 module.depth,入口模块则为 depth = 0。
this.assignDepth(module);
}
比如我们的 demo,只配置了一个入口,那么这时会生成一个 chunkGroup(Entrypoint) 和一个 chunk,这个 chunk 目前只包含入口 module。
5.2 生成 chunk graph
执行 buildChunkGraph(this, /** @type {Entrypoint[]} */ (this.chunkGroups.slice()));
buildChunkGraph
方法用于生成并优化 chunk 依赖图,建立起 module、chunk、chunkGroup 之间的关系。分为三阶段:
// /lib/buildChunkGraph.js
// PART ONE
visitModules(compilation, inputChunkGroups, chunkGroupInfoMap, chunkDependencies, blocksWithNestedBlocks, allCreatedChunkGroups);
// PART TWO
connectChunkGroups(blocksWithNestedBlocks, chunkDependencies, chunkGroupInfoMap);
// Cleaup work
cleanupUnconnectedGroups(compilation, allCreatedChunkGroups);
第一阶段 visitModules
先执行:visitModules 的 const blockInfoMap = extraceBlockInfoMap(compilation);
对本次 compliation.modules 进行一次迭代遍历,意在完完整整收集所有的模块(同步、异步)及每个模块的直接依赖。
具体处理逻辑:
遍历每个模块compilation.modules
,先把其同步依赖(dependencies
)存入 modules Set 集,再遍历异步依赖(blocks
),把每个异步依赖存入模块的 blocks 数组。
然后这些异步依赖会再加入到while
循环遍历中(作为一个模块),不仅为它在blockInfoMap
单独建立起一个ImportDependenciesBlock
类型的数据(里面包含这个异步 module 本身),再去遍历它存储一个NormalModule
类型的数据(包含它的同步 modules 和异步 blocks),之后遇到异步依赖都是优先这样处理异步依赖。
遍历结束后会建立起基本的 Module Graph,包括所有的
NormalModule
和ImportDependenciesBlock
,存储在一个blockInfoMap
Map 表当中(每一项的值都是它们的直接依赖,同步存 modules,异步存 blocks)。
以【浅析 webpack 打包流程(原理) - 案例 demo】为例,得到 blockInfoMap:
看具体数据应该能大致理解碰到异步就去迭代遍历异步的处理顺序:
// blockInfoMap
{
0: {
key: NormalModule, // a,debugId:1000,depth:0
value: {
blocks: [ImportDependenciesBlock], // 异步 c
modules: [NormalModule] // b (modules为set结构) debugId:1002,depth:1
}
},
1: {
key: ImportDependenciesBlock,
value: {
blocks: [],
modules: [NormalModule] // c,debugId:1001,depth:1
}
},
2: {
key: NormalModule, // c,debugId:1001,depth:1
value: {
blocks: [ImportDependenciesBlock], // 异步 b
modules: [NormalModule] // d,debugId:1004,depth:2
}
}
3: {
key: ImportDependenciesBlock,
value: {
blocks: [],
modules: [NormalModule] // b,debugId:1002,depth:1
}
},
4: {
key: NormalModule, // b,debugId:1002,depth:1
value: {
blocks: [],
modules: [NormalModule] // d,debugId:1004,depth:2
}
},
5: {
key: NormalModule, // d,debugId:1004,depth:2
value: {
blocks: [],
modules: []
}
}
}
存储完入口模块 a 的直接依赖(同步和异步),会优先先去循环处理它的异步依赖 c,收集 c 的直接依赖(同步和异步),然后又优先遍历 c 的异步依赖...过程中遇到的所有异步依赖都会建立一个ImportDependenciesBlock
对象,值内包含一项内容为它自身的NormalModule
。同时假如有重复的异步模块,会生成多项ImportDependenciesBlock
。其余会生成几项和 compliation.modules 一一对应的NormalModule
(a、b、c、d)
接着用reduceChunkGroupToQueueItem
函数处理目前只有一个 EntryPoint 的 chunkGroups:
// 用 reduceChunkGroupToQueueItem 处理每一个 chunkGroup
let queue = inputChunkGroups
.reduce(reduceChunkGroupToQueueItem, [])
.reverse();
将它转化为一个 queue 数组,每项为入口 module、chunk 以及对应的 action 等信息组成的对象,详见下面源码。
说明下action
:模块需要被处理的阶段类型,不同类型的模块会经过不同的流程处理,初始为 ENTER_MODULE: 1,全部类型如下:
ADD_AND_ENTER_MODULE = 0
ENTER_MODULE = 1
PROCESS_BLOCK = 2
LEAVE_MODULE = 3
紧跟着设置chunkGroupInfoMap
,它映射了每个 chunkGroup 和与它相关的信息对象。
// /lib/buildChunkGraph.js
for (const chunk of chunkGroup.chunks) {
const module = chunk.entryModule;
queue.push({
action: ENTER_MODULE, // 需要被处理的模块类型,不同处理类型的模块会经过不同的流程处理,初始为 ENTER_MODULE: 1
block: module, // 入口 module
module, // 入口 module
chunk, // seal 阶段一开始为每个入口 module 创建的 chunk,只包含入口 module
chunkGroup // entryPoint
});
}
chunkGroupInfoMap.set(chunkGroup, {
chunkGroup,
minAvailableModules: new Set(), // chunkGroup 可追踪的最小 module 数据集
minAvailableModulesOwned: true,
availableModulesToBeMerged: [], // 遍历环节所使用的 module 集合
skippedItems: [],
resultingAvailableModules: undefined,
children: undefined
});
然后基于module graph
,对 queue 进行了 2 层遍历。我们提供的 demo 是单入口,因此 queue 只有一项数据。
// /lib/buildChunkGraph.js
// 基于 Module graph 的迭代遍历,不用递归写是为了防止可能的堆栈溢出
while (queue.length) { // 外层遍历
logger.time("visiting");
while (queue.length) { // 内层遍历
const queueItem = queue.pop(); // 删除并返回 queue 数组的最后一项
// ...
if (chunkGroup !== queueItem.chunkGroup) {
// 重置更新 chunkGroup
}
switch (queueItem.action) {
case ADD_AND_ENTER_MODULE: {
// 如果 queueItem.module 在 minAvailableModules,则将该 queueItem 存入 skippedItems
if (minAvailableModules.has(module)) {
Items.push(queueItem);
break;
}
// 建立 chunk 和 module 之间的联系,将依赖的 module 存入该 chunk 的 _modules 属性里,将 chunk 存入 module 的 _chunks 里
// 如果 module 已经在 chunk 中则结束 switch
if (chunk.addModule(module)) {
module.addChunk(chunk);
}
}
case ENTER_MODULE: {
// 设置 chunkGroup._moduleIndices 和 module.index,然后
// ...
// 给 queue push 一项 queueItem(action 为 LEAVE_MODULE),供后面遍历的流程中使用。
queue.push({
action: LEAVE_MODULE,
block,
module,
chunk,
chunkGroup
});
}
case PROCESS_BLOCK: {
// 1. 从 blockInfoMap 中查询到当前 queueItem 的模块数据
const blockInfo = blockInfoMap.get(block);
// 2. 遍历当前模块的同步依赖 没有则存入 queue,其中 queueItem.action 都设为 ADD_AND_ENTER_MODULE
for (const refModule of blockInfo.modules) {
if (chunk.containsModule(refModule)) {
// 跳过已经存在于 chunk 的同步依赖
continue;
}
// 如果已经存在于父 chunk (chunkGroup 可追踪的最小 module 数据集 -- minAvailableModules)
// 则将该 queueItem push 到 skipBuffer(action 为 ADD_AND_ENTER_MODULE),并跳过该依赖的遍历
// 倒序将 skipBuffer 添加 skippedItems,queueBuffer 添加到 queue
// enqueue the add and enter to enter in the correct order
// this is relevant with circular dependencies
// 以上都不符合则将 queueItem push 到 queueBuffer(action 为 ADD_AND_ENTER_MODULE)
queueBuffer.push({
action: ADD_AND_ENTER_MODULE,
block: refModule,
module: refModule,
chunk,
chunkGroup
});
}
// 3. 用 iteratorBlock 方法迭代遍历模块所有异步依赖 blocks
for (const block of blockInfo.blocks) iteratorBlock(block);
if (blockInfo.blocks.length > 0 && module !== block) {
blocksWithNestedBlocks.add(block);
}
}
case LEAVE_MODULE: {
// 设置 chunkGroup._moduleIndices2 和 module.index2
}
}
}
// 上文 while (queue.length) 从入口 module 开始,循环将所有同步依赖都加入到同一个 chunk 里,将入口 module 及它的同步依赖里的异步依赖都各自新建了chunkGroup 和 chunk,并将异步模块存入 queueDelayed,异步依赖中的异步依赖还未处理。
while (queueConnect.size > 0) {
// 计算可用模块
// 1. 在 chunkGroupInfoMap 中设置前一个 chunkGroup 的 info 对象的 resultingAvailableModules、children
// 2. 在 chunkGroupInfoMap 中初始化新的 chunkGroup 与他相关的 info 对象的映射并设置了 availableModulesToBeMerged
if (outdatedChunkGroupInfo.size > 0) {
// 合并可用模块
// 1. 获取/设置新的 chunkGroup info 对象的 minAvailableModules
// 2. 将新的 chunkGroup info 对象的 skippedItems push 到 queue
// 3. 如果新的 chunkGroup info 对象的 children 不为空,则更新 queueConnect 递归循环
}
}
// 当 queue 队列的所有项都被处理后,执行 queueDelayed
// 把 queueDelayed 放入 queue 走 while 的外层循环,目的是在所有同步依赖 while 处理完之后,才处理异步模块
// 如果异步模块里还有异步依赖,将放到一下次的 queueDelayed 走 while 的外层循环
if (queue.length === 0) {
const tempQueue = queue; // ImportDependenciesBlock
queue = queueDelayed.reverse();
queueDelayed = tempQueue;
}
}
while 循环只要条件为 true 就会一直循环代码块,只有当条件不成立或者内部有if(condition){ return x;}
、if(condition){ break; }
才能跳出循环。( while+push 防递归爆栈,后序深度优先)
进入内层遍历,匹配到case ENTER_MODULE
,会给 queue push 一个 action 为LEAVE_MODULE
的 queueItem 项供后面遍历流程中使用。然后进入到PROCESS_BLOCK
阶段:
从blockInfoMap
中查询到当前 queueItem 的模块数据,只有当前模块的直接依赖,在本例就是:
接下来遍历模块的所有单层同步依赖 modules,跳过已经存在于 chunk 的同步依赖;如果同步依赖已在 minAvailableModules(chunkGroup 可追踪的最小 module 数据集),则将 queueItem push 到 skipBuffer,然后跳出该依赖的遍历;以上都没有则将 queueItem 存入缓冲区 queueBuffer,action 都设为 ADD_AND_ENTER_MODULE
(即下次遍历这个 queueItem 时,会先进入到 ADD_AND_ENTER_MODULE)。同步 modules 遍历完,将得到的 queueBuffer 反序添加到 queue。也就是后面的内层遍历中,会优先处理同步依赖嵌套的同步模块,(不重复地)添加完再去处理同级同步依赖。
接下来调用iteratorBlock
来迭代遍历当前模块的单层异步依赖 blocks,方法内部主要实现的是:
- 调用
addChunkInGroup
为这个异步 block 创建一个 chunk 和 chunkGroup,同时建立这两者之间的联系。此时这个 chunk 是空的,还没有添加任何它的依赖; - 把 chunkGroup 添加到
compilation.chunkGroups
(Array) 和compilation.namedChunkGroups
(Map),chunkGroupCounters(计数 Map)、blockChunkGroups(映射依赖和 ChunkGroup 关系的 Map)、allCreatedChunkGroups
(收集被创建的 ChunkGroup Set)。 - 把这项 block 和 block 所属的 chunkGroup 以对象的形式 push 到
chunkDependencies
Map 表中 ➡️ 当前 module 所属 chunkGroup (Map 的 key)下,每一都是{ block: ImportDependenciesBlock, chunkGroup: chunkGroup }
的形式。建立起 block 和它所属 chunkGroup 和 父 chunkGroup 之间的依赖关系。chunkDependencies 表主要用于后面优化 chunk graph; - 更新 queueConnect,建立父 chunkGroup 与新 chunkGroup 的映射;
- 向 queueDelayed 中 push 一个 { action:
PROCESS_BLOCK
, module: 当前 block 所属 module, block: 当前异步 block, chunk: 新 chunkGroup 中的第一个 chunk, chunkGroup: 新 chunkGroup } ,该项主要用于 queue 的外层遍历。
iteratorBlock
处理完当前模块所有直接异步依赖 (block) 后,结束本轮内层遍历。
前面为 queue push 了两项 queueItem,一个是入口模块 a(action 为 LEAVE_MODULE
),一个是同步模块 b(action 为 ADD_AND_ENTER_MODULE
)。因此继续遍历 queue 数组,反序先遍历 b,匹配到ADD_AND_ENTER_MODULE
,把 b 添加到 入口 chunk (_modules
属性)中,也把入口 chunk 存入 b 模块的_chunks
属性里。然后进入ENTRY_MODULE
阶段,标记为LEAVE_MODULE
,添加到 queue。
然后进入PROCESS_BLOCK
处理 b 的同步依赖和异步依赖(过程如上文):
尽力说得通俗些的总结:
将模块直接同步依赖标记为ADD_AND_ENTER_MODULE
添加到 queue 用于接下来的遍历,push 时其余属性 block 和 module 是它本身, chunk、chunkGroup 不变;
直接异步依赖则标记为PROCESS_BLOCK
添加到用于外层遍历的 queueDelayed,push 时传的是新的 chunk 和 chunkGroup,block 是它本身,module 是它的父模块。同时会为此异步依赖新建一个包含一个空 chunk 的 chunkGroup。
外层 while 的执行时机是等所有入口模块的同步依赖(包括间接)都处理完后。
建立初步的 chunk graph 顺序可以简单地捋成:
1.首先入口和所有(直接/间接)同步依赖形成一个 chunkGroup 组(添加模块的顺序为:先是同步依赖嵌套的同步依赖都处理完,再去遍历平级的同步依赖);
2.然后按每个异步依赖的父模块被处理的顺序,为它们各自建立一个 chunk 和 chunkGroup。异步 chunk 中只会包含入口 chunk 中不存在的同步依赖。相同的异步模块会重复创建 chunk。
然后走while (queueConnect.size > 0)
循环,更新了chunkGroupInfoMap
中父 chunkGroup 的 info 对象,初始化新的 chunkGroup info 对象,并获取了最小可用模块。
然后等内层循环把 queue 数组 (内层只管模块所有同步依赖) 一个个反序处理完(数量为0),就把 queueDelayed 赋给 queue ,走外部while(queue.length)
循环处理异步依赖 (真正处理异步模块)。这时这些 queueItem 的 action 都为PROCESS_BLOCK
,block 都为 ImportDependenciesBlock 依赖。更新 chunkGroup 后, switch 直接走 PROCESS_BLOCK 获得异步项对应的真正模块,和之前同步模块一样处理(有异步依赖就新建 chunk 和 chunkGroup [无论之前无为同样的异步块创建过 chunkGroup,均会重复创建],并放入 queueDelayed),处理数据都将存储在新的 chunkGroup 对象上。最终得到一个 Map 结构的chunkGroupInfoMap
。以 demo 为例:
children 为每项的子 chunkGroup,resultingAvailableModules 为本 chunkGroup 可用的模块
// chunkGroupInfoMap Map 对象
[
0: {
key: Entrypoint, // groupDebugId: 5000
value: {
availableModulesToBeMerged: Array(0) // 遍历环节所使用的 module 集合
children: Set(1) {} // 子 chunkGroup,groupDebugId: 5001
chunkGroup: Entrypoint
minAvailableModules: Set(0) // chunkGroup 可追踪的最小 module 数据集
minAvailableModulesOwned: true
resultingAvailableModules: Set(3) // 这个 chunkGroup 的可用模块 a b d
skippedItems: Array(0)
}
},
1: {
key: ChunkGroup, // groupDebugId: 5001
value: {
availableModulesToBeMerged: Array(0)
children: Set(1) {} // 子 chunkGroup,groupDebugId: 5002
chunkGroup: Entrypoint
minAvailableModules: Set(3) // a b d
minAvailableModulesOwned: true
resultingAvailableModules: Set(4) // 这个 chunkGroup 的可用模块 a b d c
skippedItems: Array(1) // d
}
}
2: {
key: ChunkGroup, // groupDebugId: 5002
value: {
availableModulesToBeMerged: Array(0)
children: undefined
chunkGroup: Entrypoint
minAvailableModules: Set(4) // a b d c
minAvailableModulesOwned: true
resultingAvailableModules: undefined
skippedItems: Array(1) // b
}
}
]
此时的compilation.chunkGroups
有三个 chunkGroup:
包含一个_modules: { a, b, d }
chunk 的 EntryPoint;包含一个_modules: { c }
chunk 的 chunkGroup(入口异步引入的 c 创建);包含一个空 chunk 的 chunkGroup(c 引入 b 时创建)。
即入口和它所有同步依赖组成一个 chunk(包含在 EntryPoint 内),每个异步依赖成为一个 chunk(各自在一个 chunkGroup 内)。遇到相同的异步模块会重复创建 chunk 和 chunkGroup,处理 chunk 同步模块时遇到已存在于入口 chunk 的模块将跳过,不再存入chunk._modules
。
第二阶段 connectChunkGroups
遍历 chunkDependencies,根据 ImportDependenciesBlock(block) 建立了不同 chunkGroup 之间的父子关系。
chunkDependencies
只保存有子 chunkGroup 的 chunkGroup(也就是 EntryPoint 和,有异步依赖的异步模块创建的 chunkGroup 才会被存到里面) ,属性是 chunkGroup, 值是 chunkGroup 的所有 子 chunkGroup 和 异步依赖组成的对象 的数组:
// chunkDependencies Map 对象
[
0: {
key: Entrypoint, // groupDebugId: 5000
value: [
{ block: ImportDependenciesBlock, chunkGroup: ChunkGroup }, // groupDebugId: 5001
// { block: ImportDependenciesBlock, chunkGroup: ChunkGroup }, // groupDebugId: 5003
// 实际项目一般会存在多项
]
},
1: {
key: ChunkGroup, // groupDebugId: 5001
value: [
{ block: ImportDependenciesBlock, chunkGroup: ChunkGroup } // groupDebugId: 5002
]
},
]
文字很绕,关于 chunkDependencies 用一个模块更多的图就容易理解得多了:
这个例子的 chunkDependencies 是这样的:
// 简单地用 groupDebugId 指代子 chunkgroup 和 子 chunkgroup 的 chunk
{
{ key: EntryPoint 5000, value: [5001, 5002, 5003, 5004] },
{ key: ChunkGroup 5001, value: [5005, 5006] },
{ key: ChunkGroup 5002, value: [5007] }
}
遍历时子 chunkgroup 的chunks[]._modules
如果有父 chunkGroup 的可用模块resultingAvailableModules
中不包含的新模块,则分别建立异步依赖与对应 chunkGroup(互相添加到彼此的chunkGroup
和_blocks
)、父 chunkGroup 和子 chunkGroup 的父子关系(互相添加到彼此的_children
和_parents
):
(resultingAvailableModules
通过查询chunkGroupInfoMap.get(父chunkGroup)
获取)
如上面 demo2,ChunkGroup 5001 的可用模块是a b d e c j
,它的子 ChunkGroup 5005 是由 b 创建的(且因为不会重复创建入口 chunk 中存在的同步模块, 5005 的 chunk 并不包含任何模块),没有新模块,故而没有建立起关系。而子ChunkGroup 5006 有新模块 k,就建立起了上述关系。
// /lib/buildChunkGraph.js
// ImportDependenciesBlock 与 chunkGroup 建立联系,互相添加到彼此的 chunkGroup 和 _blocks
GraphHelpers.connectDependenciesBlockAndChunkGroup(
depBlock,
depChunkGroup
);
// chunkGroup 之间建立联系:互相添加到彼此的 _children 和 _parents
GraphHelpers.connectChunkGroupParentAndChild(
chunkGroup,
depChunkGroup
);
第三阶段 cleanupUnconnectedGroups
清理无用 chunk 并清理相关的联系。
通过遍历allCreatedChunkGroups
,如果遇到在第二阶段没有建立起联系的 chunkGroup(如上面 demo2 chunkGroup 5005),那么就将这些 chunkGroup 中的所有 chunk 从 chunk graph 依赖图当中剔除掉 ( demo2 中的异步 b chunk 此时被删除 )。
allCreatedChunkGroups
即异步模块被创建的 chunkGroup,依次判断 chunkGroup 有无父 chunkGroup(_parents
),没有则执行:
// /lib/buildChunkGraph.js
for (const chunk of chunkGroup.chunks) {
const idx = compilation.chunks.indexOf(chunk);
if (idx >= 0) compilation.chunks.splice(idx, 1); // 删除 chunk
chunk.remove('unconnected');
}
chunkGroup.remove('unconnected');
同时解除 module、chunk、chunkGroup 三者之间的联系。
最终每个 module 与每个 chunk、每个 chunkGroup 之间都建立了联系,优化形成了 chunk graph。
buildChunkGraph 三阶段总结:
1.visitModules
:为入口模块和它所有(直接/间接)同步依赖形成一个 EntryPoint(继承自 ChunkGroup),为所有异步模块和它的同步依赖生成一个 chunk 和 chunkGroup(会重复)。如 chunk 的同步模块已存在于入口 chunk,则不会再存入它的_modules
。此阶段初始生成了 chunk graph(chunk 依赖图)。
2.connectChunkGroups
:检查入口 chunk 和 有异步依赖的异步 chunk, 如果它们的子 chunk 有它们未包含的新模块,就建立它们各自所属 chunkGroup 的 父子关系。
3.cleanupUnconnectedGroups
:找到没有父 chunkgroup 的 chunkgroup,删除它里面的 chunk,并解除与相关 module、chunk、chunkGroup 的关系。
2、3 阶段对 chunk graph 进行了优化,去除了 由已存在于入口 chunk 中的 模块创建的异步 chunk。
回到 Compilation.js,compilation 的 seal 方法继续执行,先将 compilation.modules 按 index 属性大小排序,然后执行:this.hooks.afterChunks.call(this.chunks)
。触发插件 WebAssemblyModulesPlugin:设置与 webassembly 相关的报错信息,到此 chunk 生成结束。
5.3 module、chunk、chunkGroup 存储字段相关
module
module 即每一个资源文件的模块对应,如 js/css/图片 等。由 NormalModule 实例化而来,存于compilation.modules
数组。
-
module.blocks
:module 的异步依赖 -
module.dependencies
:module 的同步依赖 -
module._chunks
:module 所属 chunk 列表
chunk
每一个输出文件的对应,比如入口文件、异步加载文件、优化切割后的文件等等,存于compilation.chunks
数组。
-
chunk._groups
:chunk 所属的 chunkGroup 列表 -
chunk._modules
:由哪些 module 组成
chunkGroup
默认情况下,每个 chunkGroup 都只包含一个 chunk:主 chunkGroup (EntryPoint) 包含入口 chunk,其余 chunkGroup 各包含一个异步模块 chunk。存于compilation.chunkGroups
数组。
当配置了optimization.splitChunks
,SplitChunksPlugin 插件将入口 chunk 拆分为多个同步 chunk,那么主 ChunkGroup (EntryPoint) 就会有多个 chunk 了。另外,如 runtime 被单独抽成一个文件,那么 EntryPoint 就会多出一个 runtime chunk。
-
chunkGroup.chunks
:由哪些 chunk 组成 -
chunkGroup._blocks
:异步依赖 ImportDependenciesBlock -
chunkGroup._children
:子 chunkGroup -
chunkGroup._parent
:父 chunkGroup
下文:浅析 webpack 打包流程(原理) 四 - chunk 优化
webpack 打包流程系列(未完):
浅析 webpack 打包流程(原理) - 案例 demo
浅析 webpack 打包流程(原理) 一 - 准备工作
浅析 webpack 打包流程(原理) 二 - 递归构建 module
浅析 webpack 打包流程(原理) 三 - 生成 chunk
浅析 webpack 打包流程(原理) 四 - chunk 优化
浅析 webpack 打包流程(原理) 五 - 构建资源
浅析 webpack 打包流程(原理) 六 - 生成文件
参考鸣谢:
webpack打包原理 ? 看完这篇你就懂了 !
webpack 透视——提高工程化(原理篇)
webpack 透视——提高工程化(实践篇)
webpack 4 源码主流程分析
[万字总结] 一文吃透 Webpack 核心原理
有点难的 Webpack 知识点:Dependency Graph 深度解析
webpack系列之六chunk图生成