【第1954期】 图解ChunkSplitPlugin

前言

今日早读文章由嵩恒集团秀场项目全栈开发工程师@halu886投稿分享。

正文从这开始~~

介绍

chunk(module 的集合)在 webpack 解析的依赖图中以父子关系联系起来的。最初CommonsChunkPlugin被设计用于 chunk 之间避免重复依赖,但是性能远远不是最优解。

在 webpack 4 中,内置了ChunkSplitPlugin用于替代CommonsChunkPlugin。

以下基于官方 demo进行梳理,将主要的数据结构结合图示和个人的理解进行总结。

揭开此插件的设计思路和源码神秘面纱

思路

准备

在梳理之前建议先了解一下 webpack 内部基于 tap 事件流架构。

以及 Chunk(module 的集合,也是 webpack 的打包单元,最后输出的文件就是 chunk) 和 Module(依赖树中的解析单元,可简单理解成每一个依赖的 js 文件) 的数据结构

观察 demo 中文件结构,可以简单得出以下结论

  • 共有 7 个入口文件 a ~ f.js,也就是最少七个 chunk(分别通过索引 1 ~ 7 来表示)

  • 共有 14 个 js 文件,则共有 14 个 module

  • stuff/*下的 s1.js~s8.js 和 node_modules/m1~m7.js 之间是没有相互依赖的

插件被注入在 compiler 的thisCompilation的 tap 事件以及 compilation 的optimizeModules的 tap 事件下

thisCompilation:Executed while initializing the compilation, right before emitting the compilation event.optimizeModules:Called at the beginning of the module optimization phase. A plugin can tap into this hook to perform optimizations on modules.

为了方便后续调试和梳理,特意将 demo 的 14 个文件之间的依赖关系整理出来

depend on graph

分别将 7 个入口文件 entryPoint 按颜色区分开来,箭头指向为依赖关系,橘黄色的区域为经过ChunkSplitPlugin生成的newChunk

以及将执行前的所有 Chunk 快照

chunk diagram

./webpack.config.js

  1. module.exports = {

  2. // mode: "development || "production",

  3. entry: {

  4. pageA: "./pages/a",

  5. pageB: "./pages/b",

  6. pageC: "./pages/c",

  7. pageD: "./pages/d",

  8. pageE: "./pages/e",

  9. pageF: "./pages/f",

  10. pageG: "./pages/g",

  11. },

  12. optimization: {

  13. splitChunks: {

  14. chunks: "all",

  15. maxInitialRequests: 20, // for HTTP2

  16. maxAsyncRequests: 20, // for HTTP2

  17. minSize: 40, // for example only: chosen to match 2 modules

  18. // omit minSize in real use case to use the default of 30kb

  19. },

  20. },

  21. };

前置逻辑

在插件分析整个依赖图整理 module 之间的重复依赖之前,进行了大量的额外的优化,通过空间换时间,将大部分数据状态缓存起来,

以下就通过代码顺序和逻辑一步步进行剖析。

chunkSetsInGraph

通过遍历 14 个 module,分析 module.chunksIterable 被依赖的 Chunk,通过 Chunk 的索引整合成 key 值指向 Chunk 的集合,类型为 Map<string,Set<Chunk>>

chunkSetsInGraph

例如:遍历到 m1 时,该模块被 Chunk1 和 Chunk2 以及 Chunk3 引用,则如图所示 "1,2,4"->set{Chunk1,Chunk2,Chunk3}。

当遍历到 m2 时,发现改模块也被 Chunk1 和 Chunk2 以及 Chunk3 引用,”1,2,3”已经被缓存,则跳过继续遍历

chunkSetsByCount

对chunkSetsInGraph进行遍历,将chunkSet通过 length 聚合。类型为 Map<number,Array<Set<Chunk>>>

chunkSetsByCount

例如:遍历 ChunkSetsInGrouph时"1,3"指向的 Set<Chunk>{Chunk1,Chunk3}长度为 2。则chunkSetsInGraph中推入 key 值为 2 指向[{Chunk1,Chunk3}]数组。

当遍历"2,5"时指向 Set<Chunk>{Chunk2,Chunk5}时长度为 2,则 key 值为 2 数组推入 Set<Chunk>{Chunk2,Chunk5},此时 key 值 2 指向的数组为 [{Chunk1,Chunk3},{Chunk2,Chunk5}];

combinationsCache

对所有的 ChunkSet 进行遍历,将 ChunkSet 及它的子 Set 进行聚合,通过聚合中的 ChunkSet 最大 key 进行引用。类型为 Map<string,Set<Chunk>[]>

combinationsCache

例如:遍历 chunkSetInGraph,当 key 值为1,2,3时,指向[{chunk1,chunk2,chunk3}],同时后续遍历中将所有 subSet 推入,最后"1,2,3"指向[{chunk1,chunk2,chunk3},{Chunk1,Chunk3},{Chunk1},{Chunk2},{Chunk3}]

selectedChunksCacheByChunksSet

所有的 chunkSet 对应的不同的配置过滤后的结果,基于 webpack.config.js。类型 WeakMap<Set<Chunk>, WeakMap<ChunkFilterFunction, SelectedChunksResult>>

在我们的 demo 中chunks: "all"的配置,所有的filterFunction都是const ALLCHUNKFILTER = chunk => true;,所以SelectChunkResult都是 chunkSet 本身

selectedChunksCacheByChunksSet

核心

基于以上的生成的缓存数据,接下来就是开始生成最重要的数据结构chunksInfoMap,同时遍历这个对象剥离出新的 Chunk 并且在 ChunkGroup 中建立联系。类型 Map<string,ChunksInfoItem>,ChunksInfoItem 类型为 {modules,cacheGroup,name,chunks,chunksKeys,blabla...}

对 modules,combinationCache,以及 cacheGroups 进行三层嵌套循环找出每个被依赖的 module 的重复 chunk 关系(可以尝试对照第一张图逆向推导)

cacheGroups 是中间数据, 遍历每个 module 时生成的配置项,主要用于区分 module 的类型用于过滤。例如:在chunk:all的配置下,module 默认存在配置{key:normal,minChunk: 2},但是node_modules/*下的 m1 ~ m7 除了默认配置额外{key:vendor,minChunk: 1}

例如:遍历到 m1 时,存在两个 cacheGroup 配置项[{key:normal},{key:vendors}],且在 combinationCache 被{chunk1,chunk2,chunk3} 及子集[{chunk1,chunk3},{chunk1},{chunk2},{chunk3}]依赖。所以基于 key 值分别推入 7 条数据(另外 3 条由于 vendor 的 minChuck 为 2 被过滤)

chunksInfoMap

最后对chunksInfoMap循环检索

在每次循环检索,推出优先级最高的ChunksInfoItem(通过 name/path/chunks.lengthd 长度等等)。

通过校验后,使用compilation.addChunk生成一个 newChunk,并且GraphHelpers.connectChunkAndModule将 newChunk 注入 Graph 中,并且通过chunk.removeModule移走旧 chunk 中 newChunk 中的 module。

直至 chunksInfoMap 为空

注意:进入下一次循环前,会将剩余的 ChunksInfoItem 的 module 进行清理,已被封装进入 newChunk 的 module 移除,如果 ChunksInfoItem 的 module 为 0,则这个 chunkInfoItem 被 delete

总结

webpack 极大的解放了前端工程化的劳动力,一个插件中的逻辑都能给予我们大量的启发,并且对于阅读知名的开源项目,源码结构也确实十分严谨和美观,收获颇丰。

由于 demo 比较基础,插件中很多边界情况并没有涉及到,但是主流程梳理的比较完善。

研习大佬的代码,就像阅读一本著作,虽然很痛苦但是却很享受~

关于本文 作者:@阿春 原文:http://www.halu886.cn/2020/05/18/chunkSplitPlugin-sumary/

为你推荐


【第1911期】图解常用的 Git 指令含义


【第1790期】图解Event Loop


【第961期】图解 React Virtual DOM


欢迎自荐投稿,前端早读课等你来

评论