浅析 webpack 打包流程(原理) 三 - 生成 chunk

接上文:浅析 webpack 打包流程(原理) 二 - 递归构建 module

五、生成 chunk

生成 chunk 阶段概述:在compilation.finish回调中执行的 seal 方法中,触发海量钩子,就此侵入 webpack 的封包阶段;
1.首先对所有 importexport 做标记,以实现最后构建资源阶段的 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,包括所有的NormalModuleImportDependenciesBlock,存储在一个blockInfoMap Map 表当中(每一项的值都是它们的直接依赖,同步存 modules,异步存 blocks)。
以【浅析 webpack 打包流程(原理) - 案例 demo】为例,得到 blockInfoMap:

Map结构,一共6项,未截完全

看具体数据应该能大致理解碰到异步就去迭代遍历异步的处理顺序:

// 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 的模块数据,只有当前模块的直接依赖,在本例就是:

blockInfo

接下来遍历模块的所有单层同步依赖 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,方法内部主要实现的是:

  1. 调用addChunkInGroup为这个异步 block 创建一个 chunk 和 chunkGroup,同时建立这两者之间的联系。此时这个 chunk 是空的,还没有添加任何它的依赖;
  2. 把 chunkGroup 添加到compilation.chunkGroups(Array) 和compilation.namedChunkGroups(Map),chunkGroupCounters(计数 Map)、blockChunkGroups(映射依赖和 ChunkGroup 关系的 Map)、allCreatedChunkGroups(收集被创建的 ChunkGroup Set)。
  3. 把这项 block 和 block 所属的 chunkGroup 以对象的形式 push 到 chunkDependencies Map 表中 ➡️ 当前 module 所属 chunkGroup (Map 的 key)下,每一都是{ block: ImportDependenciesBlock, chunkGroup: chunkGroup }的形式。建立起 block 和它所属 chunkGroup 和 父 chunkGroup 之间的依赖关系。chunkDependencies 表主要用于后面优化 chunk graph;
  4. 更新 queueConnect,建立父 chunkGroup 与新 chunkGroup 的映射;
  5. 向 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

初步的 chunk graph
第二阶段 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 用一个模块更多的图就容易理解得多了:

多模块 demo2

这个例子的 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

此时的 的 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图生成

你可能感兴趣的:(浅析 webpack 打包流程(原理) 三 - 生成 chunk)