当前位置: 首页 > 科技观察

《Webpack》从0到1学习CodeSplitting

时间:2023-03-21 22:49:01 科技观察

转载本文请联系唯一大学前端技术公众号。一、前言在默认配置中,我们知道webpack会将所有的代码打包成一个chunk。比如当你的单页应用非常大的时候,你可能需要把每条路由拆分成一个chunk,这样方便我们实现按需加载。代码分离是webpack最引人注目的特性之一。此功能可以将代码分成不同的包,然后可以按需或并行加载。代码分离可以用来获取更小的bundle,控制资源的加载优先级。如果使用得当,会极大地影响加载时间。2.关于代码分割接下来我们将分析不同代码分割方式带来的封装差异。首先,我们的项目假设有两个简单的文件??index.jsimport{mul}from'./test'import$from'jquery'console.log($)console.log(mul(2,3))test。jsimport$from'jquery'console.log($)functionmul(a,b){returna*b}export{mul}可以看出现在两者都依赖于jquery库,并且还会相互依赖.当我们在默认配置下打包时,结果是这样的??,所有的内容都会被打包成一个主包(324kb)。那么我们如何以最直接的方式将其他模块从这个bundle中分离出来呢?1.multi-entrywebpack配置中的entry可以设置为multiple,也就是说我们可以分别使用index和test文件作为entry://entry:'./src/index.js',原来是单个entry/**现在分别作为入口使用*/entry:{index:'./src/index.js',test:'./src/test.js'},output:{filename:'[name].[hash:8].js',path:path.resolve(__dirname,'./dist'),}那么我们来看看这个打包的结果:确实打包了两个文件!但是为什么两个文件都有320+kb呢?这不是意味着很容易分裂成更小的束吗?这是因为两者都引入了jquery,而webpack每次从两个入口分析打包时,都会分别打包依赖的模块。是的,这种配置方式确实带来了一些隐患和不便:如果entrychunk之间有一些重复的模块,那么这些重复的模块会被引入到每个bundle中。这种方式不够灵活,没有动态地将代码从核心应用逻辑中分离出来。那么有没有办法将相互依赖的模块打包分离,而不需要手动配置入口的繁琐呢?必须有。2.SplitChunksPluginSplitChunks是webpack5自带的开箱即用的插件。它可以分离符合规则的块,或者自定义配置。它在webpack5中用于替换CommonsChunkPlugin,用于解决webpack4中的重复依赖。让我们在我们的webpack配置中添加一些配置:entry:'./src/index.js',//这里我们改回单入口/**添加以下设置*/optimization:{splitChunks:{chunks:'all',},},打包后的结果如图:可以看出除了根据入口打包的mainbundle外,还多了一个vendors-node_modules_jquery_dist_jquery_js.xxxxx.js,显然这样我们就提取公共jquery模块。接下来让我们探索SplitChunksPlugin。先看配置的默认值:splitChunks:{//表示选择哪些chunk进行拆分,可选值有:async,initial和allchunks:"async",//表示新分离的chunk必须更大大于或等于minSize,20000,约20kb。minSize:20000,//通过保证分裂后剩余的最小chunk体积超过限制来避免零尺寸模块,只有当单个chunk剩余时才生效minRemainingSize:0,//表示一个模块至少要有minChunks个chunk才能被分割。默认为1。minChunks:1,//表示按需加载文件时的最大并行请求数。maxAsyncRequests:30,//表示加载入口文件时的最大并行请求数。maxInitialRequests:30,//强制拆分音量阈值等限制(minRemainingSize,maxAsyncRequests,maxInitialRequests)将被忽略enforceSizeThreshold:50000,//cacheGroups下可以配置多个组,每个组可以根据测试设置条件,满足的模块测试条件分配给该组。一个模块可以被多个组引用,但最终会根据优先级决定打包到哪个组。默认情况下,node_modules目录下的所有模块都被打包到vendors组,超过两个chunk共享的模块被打包到default组。cacheGroups:{defaultVendors:{test:/[\\/]node_modules[\\/]/,//一个模块可以属于多个缓存组。优化将优先考虑具有更高优先级的缓存组。priority:-10,//如果当前chunk中包含从mainbundle中分离出来的模块,会被重用reuseExistingChunk:true,},default:{minChunks:2,priority:-20,reuseExistingChunk:true}}}默认情况下,SplitChunks只会拆分异步调用的模块(chunks:"async"),默认情况下处理的chunk必须至少为20kb,太小的模块将不会被包含。添加,默认值会根据模式的配置而有所不同。具体见源码:const{splitChunks}=optimization;if(splitChunks){A(splitChunks,"defaultSizeTypes",()=>["javascript","unknown"]);D(splitChunks,"hidePathInfo",production);D(splitChunks,"chunks","async");D(splitChunks,"usedExports",optimization.usedExports===true);D(splitChunks,"minChunks",1);F(splitChunks,"minSize",()=>(production?20000:10000));F(splitChunks,"minRemainingSize",()=>(development?0:undefined));F(splitChunks,"enforceSizeThreshold",()=>(production?50000:30000));F(splitChunks,"maxAsyncRequests",()=>(production?30:Infinity));F(splitChunks,"maxInitialRequests",()=>(production?30:Infinity));D(splitChunks,"automaticNameDelimiter","-");const{cacheGroups}=splitChunks;F(cacheGroups,"默认",()=>({idHint:"",reuseExistingChunk:true,minChunks:2,priority:-20}));F(cacheGroups,"defaultVendors",()=>({idHint:"vendors",reuseExistingChunk:true,test:NODE_MODULES_REGEXP,priority:-10}));}cacheGroups缓存group是splitChunks中最重要的部分。他可以使用splitChunks.*中的任何选项,但是test、priority和reuseExistingChunk只能在缓存组级别配置。默认配置已经为我们提供了一个Vendors组和一个defalut组,在**Vendors**组中使用test:/[\\/]node_modules[\\/]/来匹配node_modules中所有符合规则的模块。提示:当webpack处理文件路径时,它们在Unix系统上总是包含/,在Windows系统上总是包含\。这就是为什么在{cacheGroup}.test字段中使用[\/]来表示路径分隔符的原因。/或\in{cacheGroup}.test跨平台使用时会出现问题。总结上面的配置,我们就可以理解为什么在打包的时候会生成一个名为vendors-node_modules_jquery_dist_jquery_js.db47cc72.js的文件。如果想自定义名字,也可以使用splitChunks.name属性(每个cacheGroup都有),支持三种形式:boolean=false设置为false,保持chunk同名,所以不要随意改名.这是生产构建的推荐值。function(module,chunks,cacheGroupKey)=>string要求返回值是字符串类型,chunks数组中的每个chunk都有chunk.name和chunk.hash属性,例如name(module,chunks,cacheGroupKey){constmoduleFileName=module.identifier().split('/').reduceRight((item)=>item);constallChunksNames=chunks.map((item)=>item.name).join('~');return`${cacheGroupKey}-${allChunksNames}-${moduleFileName}`;},string指定一个字符串或一个总是返回相同字符串的函数会将所有通用模块和供应商组合成一个块。这可能会导致更大的初始下载并减慢页面加载速度。另外注意splitChunks.maxAsyncRequests和splitChunks.maxInitialRequests指的是按需加载的最大并行请求数和初始页面渲染的最大并行请求数。当我们的项目很大的时候,如果我们需要分离一个依赖包,可以这样配置:cacheGroups:{react:{name:'react',test:/[\\/]node_modules[\\/](react)/,chunks:'all',priority:-5,},},以便指定包在打包后可以拆分:更多配置细节请参考官网配置文档3.动态导入使用import()语法来实现动态导入。这也是我们强烈推荐的代码拆分方法。我们先简单修改一下我们的index.js,然后看看使用后打包的效果://import{mul}from'./test'import$from'jquery'import('./test').then(({mul})=>{console.log(mul(2,3))})console.log($)//console.log(mul(2,3))可以看到通过import()语法导入打包时模块会自动单独打包。值得注意的是,这种语法还有一个非常方便的“动态引用”方法,可以添加一些合适的表达式。例如,假设我们需要加载一个合适的主题:constthemeType=getUserTheme();import(`./themes/${themeType}`).then((module)=>{//dosthaboouttheme});这样我们就可以“动态地”加载我们需要实现的异步模块了,原理在于两点:至少需要包含模块相关的路径信息,并且可以限定打包到特定的目录或者文件放。webpack会根据路径信息,在打包的时候将./themes中的所有文件打包成一个新的chunk,以便需要的时候使用。4.魔术注释在上面的import()语法中,我们会发现打包自动生成的文件名并不是我们想要的。我们如何控制包的名称?这里介绍一下我们的魔术注释(MagicComments):import(/*webpackChunkName:"my-chunk-name"*/'./test')这样打包的文件:魔术注释不仅可以帮我们修改chunkname,还要实现预加载等功能,这里举个例子:我们希望在点击按钮的时候加载我们需要的模块功能,代码可以这样://index.jsdocument.querySelector('#btn').onclick=function(){import('./test').then(({mul})=>{console.log(mul(2,3));});};//test.jsfunctionmul(a,b){returna*b;}console.log('测试已加载');export{mul};可以看到我们点击按钮的时候确实加载了test.js的文件资源。但如果模块较大,点击即加载可能会导致加载时间长等用户体验不佳。这时候我们可以使用我们的/*webpackPrefetch:true*/方法进行预取,我们看看效果://index,jsdocument.querySelector('#btn').onclick=function(){import(/*webpackPrefetch:true*/'./test').then(({mul})=>{console.log(mul(2,3));});};可以看到在整个过程中,在初始加载屏幕的时候,test.js的资源已经被预加载,当我们点击按钮的时候,会从(prefetchcache)中读取内容。这就是模块预取的过程。另外,我们还有/*webpackPreload:true*/的方式来预加载。但是prefetch和preload听起来很像,实际上它们的加载时机是完全不同的:preloadchunk会在parentchunk加载完成后开始并行加载。预取块将在父块完成加载后开始加载。预加载块具有中等优先级并立即下载。浏览器空闲时下载预取块。当前时刻在父块中立即请求预加载块。预取块将在将来的某个时候使用。3.最后,我们一开始有工程打包的想法时,会考虑将多个文件打包成一个文件,减少多次资源请求。随着项目越来越复杂,在做项目优化的时候,我们发现项目加载的时间越长,用户的体验越差,所以可以通过代码拆分的方式来减少请求时过多的资源量该页面最初已加载。本文仅简单介绍常用的webpack代码拆分方式,但在实际项目中进行性能优化时,往往会有更苛刻的要求。希望通过本文的介绍,让你快速了解代码拆分入门的技巧和优势。参考HowtousesplitChunkstofinelycontrolcodesplittingCodeSplitting-Webpack