美文网首页
浅析 webpack 打包流程(原理) 三 - 生成 chunk

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

作者: AizawaSayo | 来源:发表于2021-06-09 23:57 被阅读0次

    接上文:浅析 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.splitChunksSplitChunksPlugin 插件将入口 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

          本文链接:https://www.haomeiwen.com/subject/bhfrsltx.html