接上文:浅析 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图生成