likes
comments
collection
share

Webpack5源码解读系列3 - 分析产物组织形式

作者站长头像
站长
· 阅读数 102

概念

Webpackmake阶段构建了文件依赖关系的映射ModuleGraph,接下来进入seal阶段,seal意为“密封”,即将模块代码封装起来。而在封装之前,需要明确产物文件组织形式,Webpack通过ModuleGraph分析出产物加载顺序,从而得到产物文件引用关系图ChunkGraph,并最终用于Chunk文件输出。

ChunkGraph构建流程之前,需要先浅介绍一下几个领域模型:

Chunk

Chunk是模块的封装,内部存放多个模块。Chunk可以与输出文件画上等号,一个Chunk最终会输出为一个产物文件,

ChunkGroup

ChunkGroup可以是一个或者多个Chunk,用于维护Chunk的父子关系(Chunk的父子关系决定了最后的加载顺序)。

ChunkGraph

ChunkGroup存在父子关系,并最后形成一张DAG,由ChunkGraph维护。ChunkGraph为后续代码生成提供查询能力。

构建过程

ChunkGroup分包规则

在了解ChunkGraph构建过程之前,需要前置了解ChunkGroup的生成规则,其中可以分为三种生成规则(分包规则),分别是Entry分包、异步分包、Runtime分包。

Entry分包

Webpack入口配置会生成EntryPointEntryPoint继承于ChunkGroup,专门处理入口维度的ChunkGroup,下面统一将EntryPoint称为ChunkGroup。入口可以配置同步或者异步入口:

module.exports = {
  entry: {
    entry1: {
      import: 'A.js',
    },
    entry2: {
      import: 'B.js',
      // 依赖于 entry1 入口加载,是一异步模块加载
      dependOn: ['entry1']
    }
  }
}

配置entry字段中配置的每个入口都会处理为一个EntryPoint,如果存在dependOn字段,那么会处理为一个具有父子关系的EntryPoint。上述入口配置最终会生成两个Chunk

异步分包

在代码中使用require.ensure('xxx')或者import('xxx')模块都能够使用代码分割能力,代码分割能力底层都是将目标代码从本Chunk分离并成为一个独立Chunk,在运行时异步加载代码。

import './Home.js';

// 使用异步加载能力,会将 target.js模块从本模块所属chunk独立出去
import('./target.js');

异步加载能力在处理时会创建新的ChunkGroup并与引用方的ChunkGroup形成父子关系,父子关系在文件输出时体现为加载顺序。

Runtime分包

默认地,Webpack应用运行时代码都会放到应用入口Chunk上,这是为了能够让所有代码都能够使用运行时能力。如果一个项目存在多个入口,那么可以选择将Runtime相关能力独立出来,减少代码冗余:

module.exports = {
  entry: {
    entry1: {
      import: 'A.js',
      runtime: 'runtime'
    },
    entry2: {
      import: 'B.js',
      runtime: 'runtime',
    }
  }
}

上面配置编译时,会产生三个ChunkGroup,分别是entry1entry2runtime,编译产物中runtime.js文件同时被entry1.jsentry2.js引用。

ChunkGraph构建

遍历模块

从入口开始,借助ModuleGraph遍历应用模块拓扑图,此操作有两个目的:

  1. 绑定模块与ChunkGroup,而ChunkGroup内部可以包含多个Chunk,实际上是绑定了模块与Chunk的关系;
  2. 获取每个模块的异步导入语法,创建新的ChunkGroup以及异步模块对应的Chunk

从入口开始,获取入口模块,并将其推入队列中,接下来对于执行队列中每个元素都做以下操作:

  • 从队列中取出模块,将模块和对应的ChunkGroup绑定起来,此操作同时绑定了模块和Chunk,之后将本模块所导入的所有子模块推进执行队列中等待执行;
  • 如果模块存在blocks属性(blocks代表本模块所使用的异步模块),那么会解析block并根据block内容生成ChunkGroup,此时此时将block指向的入口Module推进队列中,等待遍历;

Webpack5源码解读系列3 - 分析产物组织形式

ChunkGroup关系处理

有两种方式决定ChunkGroup关系:

  1. 入口配置设置dependOn属性表明一个EntryPoint依赖于哪些入口,由此决定父子关系;
  2. 异步引入会产生一个ChunkGroup,此ChunkGroup会与原ChunkGroup绑定父子关系;

ChunkGroup父子关系代表着ChunkGroup所影响模块加载顺序,ChunkGroup绑定父子关系之后会形成树状结构,后者加载依赖于前者加载完成,如下面的case:

Webpack5源码解读系列3 - 分析产物组织形式

上述例子中由于ChunkGroup2ChunkGroup3包含了同样的ModuleChunk也相同),所以最终只会产生一份文件,如果将ChunkGroup2ChunkGroup3进行合并会形成一张有向无环图(DAG),代表着运行时文件加载顺序:

Webpack5源码解读系列3 - 分析产物组织形式

冗余模块去除

最小可用模块集合作用

ModuleGraph遍历完毕后,Webpack已经创建了项目中可能存在的所有ChunkGroup,此时可能存在冗余的ChunkGroup,如果不做精简,那么会造成生成代码冗余。冗余来源于重复模块代码,如存在以下模块引用关系,ChunkGroup1ChunkGroup2同时共享B模块,此时B模块会存在两个产物副本:

Webpack5源码解读系列3 - 分析产物组织形式

此时需要做一些优化,将模块B从ChunkGroup2去除,在ChunkGroup1保留,这样做不会影响到运行时表现。

为什么是从 ChunkGroup2 中移除而不是从 ChunkGroup1 中移除呢

ChunkGroup最终会形成一张有向无环图(DAG),应用初始化加载时会以其中某个拓扑排序顺序加载,也就是说ChunkGroup2对应的Chunk一定会先于ChunkGroup1对应Chunk加载,所以将B模块归属于ChunkGroup1能够保证应用所有地方都能访问到B模块。构建过程使用了计算最小可用集合算法去除冗余。

计算最小可用模块集合

最小可用集合算法实现起来很复杂,但是能用一句话概括:某个ChunkGroup的最小可用集合是从入口到其节点的必经之路所包含的所有模块。举一个例子说明一下:

Webpack5源码解读系列3 - 分析产物组织形式

上面是由ChunkGroup组成的DAG,从图中我们能够得出每个ChunkGroup的最小可用模块集合:

名称最小可用模块集合解释
ChunkGroup1
ChunkGroup2A
ChunkGroup3A
ChunkGroup4A、BChunkGroup4是由ChunkGroup2异步导入,ChunkGroup1ChunkGroup2都是其必经之路
ChunkGroup5A、CChunkGroup4是由ChunkGroup3异步导入,ChunkGroup1ChunkGroup3都是其必经之路
ChunkGroup6A、D对于ChunkGroup6来说,只有ChunkGroup1ChunkGroup4ChunkGroup5是必经之路(ChunkGroup4ChunkGroup5具有相同的Chunk,后续会处理成一个ChunkGroup

在极端情况下可能一个ChunkGroup的所有模块都会被清除,此时ChunkGroup也会连带被清除,下图中ChunkGroup1包含了ChunkGroup2所有模块,通过最小可用集合可以得出ChunkGroup2并不包含任何模块,最终会将ChunkGroup2清除:

Webpack5源码解读系列3 - 分析产物组织形式

小结

分析产物组织形式需要关注两方面内容:

  • ChunkGroup分包规则:

    • Entry分包:使用多个入口配置最终会产生多个文件
    • 异步分包:使用异步导入产生多一个ChunkGroup
    • Runtime分包:使用runtime字段将runtime代码单独分包
  • ChunkGraph构建

ChunkGraph通过遍历ModuleGraph从而获取到所有ChunkGroup,并通过建立父子关系从而构建ChunkGraph,构建完毕后还会通过计算最小可用模块集合的方式去除冗余模块。

转载自:https://juejin.cn/post/7231805092272980023
评论
请登录