背景前面系列文章提到,在webpack的实现中,原始资源模块以Module对象的形式存在、循环、解析。Chunk是输出产品的基本组织单元。在生成阶段,webpack将entry和其他Module按照规则插入到Chunk中,然后SplitChunksPlugin插件根据优化规则和ChunkGraph对Chunk进行一系列的更改、反汇编、合并操作。重组为一批(可能)性能更高的块。运行后webpack继续将chunk一个一个写入物理文件,完成编译工作。综上所述,Module主要用于webpack编译过程的前半部分,解决“如何读取”原始资源的问题;而Chunk对象主要用在编译的后半段,解决编译后的产物“怎么写”的问题,两者配合构建webpack构建主流程。Chunk的排列规则非常复杂,涉及到入口、优化等诸多配置项。我打算分两篇来讲解基本分包规则和SplitChunksPlugin分包优化规则。本文将着重于第一部分讲解入口、异步模块、运行时这三大规则的细节和原理。默认分包规则Webpack4之后,编译过程大致可以拆解为四个阶段(参考:【万字总结】一文看懂Webpack核心原理):在构建(make)阶段,webpack从入口,根据模块之间的引用关系(require/import)逐步构建模块依赖图(ModuleDependencyGraph)。依赖图表达了模块之间相互引用的顺序。基于这个顺序,webpack可以在模块运行之前推断出哪些依赖模块需要被执行,并且可以进一步推断出哪些模块应该打包在一起,哪些模块可以稍后加载(异步执行)。关于模块依赖图的更多信息,可以参考我的另一篇文章《有点难的 webpack 知识点:Dependency Graph 深度解析》。在生成(seal)阶段,webpack会根据模块依赖图的内容——Chunk对象来组织分包。默认的分包规则是:同一入口下到达的模块组织成一个chunk异步模块单独组织成一个chunkentry.runtime单独组织成一个chunk。默认规则集中在compilation.seal函数中。seal核心逻辑运行后,会生成一系列的Chunk、ChunkGroup、ChunkGraph对象。后续的SplitChunksPlugin等插件会进一步拆解Chunk系列对象。、优化,最后体现在输出中会显示复杂的分包结果。让我们谈谈默认构建规则。Entry分包处理的重点:seal阶段遍历entry对象,为每个entry分别生成一个chunk,然后根据模块依赖图将entry接触到的所有模块打包到chunk中。在生成阶段,Webpack首先根据遍历用户提供的entry属性值,为每一个entry创建一个Chunk对象,例如如下配置:module.exports={entry:{main:"./src/main",home:"./src/home",}};webpack遍历入口对象属性,创建两个对象chunk[main]和chunk[home]。此时两个chunk分别包含main和home模块:初始化后,Webpack会读取ModuleDependencyGraph的内容,将entry对应的内容塞入对应的chunk中(发生在webpack/lib/buildChunkGrap.js文件中)。例如对于以下文件依赖:main.js直接或间接同步引用了a/b/c/d这四个文件,分析ModuleDependencyGraph过程会逐步添加a/b/c/d模块到chunk[main],最终形成:PS:基于动态加载生成的chunk在webpack官方文档中通常被称为“Initialchunk”。异步模块分包处理要点:在分析ModuleDependencyGraph时,每遇到一个异步模块,都会为其创建一个单独的Chunk对象,将异步模块单独打包。Webpack4之后,只需要使用异步语句require.ensure("./xx.js")或者import("./xx.js")引入模块,就可以实现模块的动态加载.这个能力的本质也是基于Chunk实现的。在Webpack生成阶段,当遇到异步import语句时,会为该模块单独生成一个chunk对象,并将其所有子模块添加到该chunk中。比如下面这个例子://index.js,入口文件import'sync-a'import'sync-b'import('async-c')在index.js中,sync-a,sync-b;异步引入async-a模块;同时在async-a中同步引入sync-c模块。对应的模块依赖如下:此时webpack会分别为入口index.js和异步模块async-a.js创建子包,形成如下数据:这里需要引入一个新的概念——parent-Chunk之间的子关系。entry生成的Chunk是相互隔离的,没有必要的前后台依赖,但是异步生成的Chunk就不一样了。referer(上例中的index.js块)需要在特定场景下使用引用(上例中的async-aChunk),两者之间存在单向依赖。在webpack中,referrer称为parent,被引用的是child,分别存储在ChunkGroup._parents和ChunkGroup._children属性中。上述分包方案默认会生成两个文件:index.js对应入口index和src_async-a_js.js对应异步模块async-a运行时,webpack在index.js中使用promise和__webpack_require__.e方法异步加载运行src_async-a_js.js文件,实现动态加载。PS:基于异步模块的chunk在webpack官方文档中通常被称为“Asyncchunks”。Runtime分包重点:Webpack5之后,可以根据entry.runtime配置单独打包runtime代码。除了入口和异步模块,webpack5及之后的版本还支持基于运行时的分包规则。Webpack编译产品除了业务代码外,还需要包含一些配套代码,以支持webpack模块化、异步加载等特性。这类代码在webpack中统称为runtime。例如,产品通常包含以下代码:/******/(()=>{//webpackBootstrap/******/var__webpack_modules__={};//Themodulecache/********************************************************************************//******//******/var__webpack_module_cache__={};//需要函数/******//******//******/function__webpack_require__(moduleId){/******//******/__webpack_modules__[moduleId](module,module.exports,__webpack_require__);//Returntheexportsofthemodule/******//******//******/returnmodule.exports;/******/}//exposethemodulesobject(__webpack_modules__)/******//******//******/__webpack_require__.m=__webpack_modules__;/*webpack/runtime/compatgetdefaultexport*//******///...})();编译时,Webpack会根据业务代码决定输出支持特性(基于Dependency子类)的运行时代码,例如:__webpack_require__.f、__webpack_require__.r等函数需要实现最小模块化支持如果你使用动态加载特性,需要编写__webpack_require__.e函数。如果使用ModuleFederation特性,需要编写__webpack_require__.o函数等,虽然每段运行时代码可能很小,但是随着特性的增加,最终的结果会越来越大,尤其是对于多入口的应用,每次入口都重复打包类似的运行时代码,有点浪费。这个webpack5专门提供了entry.runtime配置项来声明如何打包runtime代码在使用上,只要在入口项中加入字符串形式的运行时值即可,例如:module.exports={entry:{index:{import:"./src/index",runtime:"solid-runtime"},}};执行entry和异步模块分包后,Webpack开始遍历entry配置,判断其是否具有runtime属性,如果有,则创建一个以runtime值命名的Chunk。因此,上面例子中的配置会生成两个chunk:chunk[index.html]。js],chunk[solid-runtime],最后据此产生两个文件:入口索引对应的index.js文件和运行时配置对应的solid-runtime.js文件。两者设置相同的运行时值,webpack运行时代码最终会写入同一个chunk。例如,对于以下配置:module.exports={entry:{index:{import:"./src/index",runtime:"solid-runtime"},home:{import:"./src/home",runtime:"solid-runtime"},}};entryindex和home共享runtime,最终生成三个chunk,分别为:同时生成三个文件:index.js对应entryindexhome.js对应entryindexsolid-runtime.js分包规则对应运行时代码至此,webpack分包规则的基本逻辑就介绍完了。实现上,大部分功能代码集中在:webpack/lib/compilation.js文件的seal函数webpack/lib/buildChunkGraph.js文件的buildChunkGraph函数默认分包规则最大的问题是无法解决modules的重复,如果多个chunk同时包含同一个module,那么这个module会被重复打包到这些c中hunk例如,假设我们有两个main/index条目同时依赖同一个模块:默认情况下,webpack不会对此做额外的处理,只是简单地将c模块打包到main/index的两个chunk中同时,finally形式:可见chunk之间是相互隔离的,模块c被反复打包,可能对最终产品造成不必要的性能损失!为了解决这个问题,webpack3引入了CommonChunkPlugin插件,试图将entry之间的公共依赖提取到单独的chunk中,但是CommonChunkPlugin本质上是基于Chunk之间的简单父子关系链来实现的。很难推断提取出的第三个包应该作为条目的父块还是子块。CommonChunkPlugin被统一视为父块。在某些情况下,对性能有不小的负面影响。webpack4之后,引入了更负责的设计——ChunkGroup专门实现了关系链管理,配合SplitChunksPlugin,可以更高效、更智能地实现“启发式分包”。这里的内容很复杂,打算下篇文章拆解再说吧。
