源自最近对业务项目进行 webpack 异步分包加载一点点的学习总结
提纲如下:
- 相关概念
- webpack 分包配置
- webpack 异步加载分包如何实现
相关概念
- module、chunk、bundle 的概念
先来一波名词解释。先上网上一张图解释:
通过图可以很直观的分出这几个名词的概念:
1、module
:我们源码目录中的每一个文件,在 webpack 中当作module
来处理(webpack 原生不支持的文件类型,则通过 loader 来实现)。module
组成了chunk
。
2、chunk
。webpack
打包过程中的产物,在默认一般情况下(没有考虑分包等情况),x 个webpack
的entry
会输出 x 个bundle
。
3、bundle
。webpack
最终输出的东西,可以直接在浏览器运行的。从图中看可以看到,在抽离 css(当然也可以是图片、字体文件之类的)的情况下,一个chunk
是会输出多个bundle
的,但是默认情况下一般一个chunk
也只是会输出一个bundle
hash
、chunkhash
、contenthash
这里不进行 demo 演示了,网上相关演示已经很多。
hash。所有的 bundle 使用同一个 hash 值,跟每一次 webpack 打包的过程有关
chunkhash。根据每一个 chunk 的内容进行 hash,同一个 chunk 的所有 bundle 产物的 hash 值是一样的。因此若其中一个 bundle 的修改,同一 chunk 的所有产物 hash 也会被修改。
contenthash。计算与文件内容本身相关。
tips:需要注意的是,在热更新
模式下,会导致chunkhash
和contenthash
计算错误,发生错误(Cannot use [chunkhash] or [contenthash] for chunk in '[name].[chunkhash].js' (use [hash] instead)
)。因此热更新下只能使用hash
模式或者不使用hash
。在生产环境中我们一般使用contenthash
或者chunkhash
。
说了这么多,那么使用异步加载/分包加载有什么好处呢。简单来说有以下几点
1、更好的利用浏览器缓存。如果我们一个很大的项目,不使用分包的话,每一次打包只会生成一个 js 文件,假设这个 js 打包出来有 2MB。而当日常代码发布的时候,我们可能只是修改了其中的一行代码,但是由于内容变了,打包出来的 js 的哈希值也发生改变。浏览器这个时候就要重新去加载这个 2MB 的 js 文件。而如果使用了分包,分出了几个 chunk,修改了一行代码,影响的只是这个 chunk 的哈希(这里严谨来说在不抽离 mainifest 的情况下,可能有多个哈希也会变化),其它哈希是不变的。这就能利用到 hash 不变化部分代码的缓存
2、更快的加载速度。假设进入一个页面需要加载一个 2MB 的 js,经过分包抽离后,可能进入这个页面变成了加载 4 个 500Kb 的 js。我们知道,浏览器对于同一域名的最大并发请求数是 6 个(所以 webpack 的maxAsyncRequests
默认值是 6),这样这个 4 个 500KB 的 js 将同时加载,相当于只是穿行加载一个 500kb 的资源,速度也会有相应的提高。
3、如果实现的是代码异步懒加载。对于部分可能某些地方才用到的代码,在用到的时候才去加载,也能很好起到节省流量的目的。
webpack 分包配置
在这之前,先强调一次概念,splitChunk
,针对的是chunk
,并不是module
。对于同一个 chunk 中,无论一个代码文件被同 chunk 引用了多少次,它都还是算 1 次。只有一个代码文件被多个 chunk 引用,才算是多次。
webpack 的默认分包配置如下
module.exports = {
optimization: {
splitChunks: {
// **`splitChunks.chunks: 'async'`**。表示哪些类型的chunk会参与split。默认是异步加载的chunk。值还可以是`initial`(表示入口同步chunk)、`all`(相当于`initial`+`async`)。
chunks: "async",
// minSize 表示符合代码分割产生的新生成chunk的最小大小。默认是大于30kb的才会生成新的chunk
minSize: 30000,
// maxSize 表示webpack会尝试将大于maxSize的chunk拆分成更小的chunk,拆解后的值需要大于minSize
maxSize: 0,
// 一个模块被最少多少个chunk共享时参与split
minChunks: 1,
// 最大异步请求数。该值可以理解为一个异步chunk,被抽离出同时加载的chunk数不超过该值。若为1,该异步chunk将不会抽离出任意代码块
maxAsyncRequests: 5,
// 入口chunk最大请求数。在多entry chunk的情况下会用到,表示多entry chunk公共代码抽出的最大同时加载的chunk数
maxInitialRequests: 3,
// 初始chunk最大请求数。
// 多个chunk拆分出小chunk时,这个chunk的名字由多个chunk与连接符组合成
automaticNameDelimiter: "~",
// 表示chunk的名字自动生成(由cacheGroups的key、entry名字)
name: true,
// cacheGroups 表示分包分组规则,每一个分组会继承于default
// priority表示优先级,一个chunk可能被多个分组规则命中时,会使用优先级较高的
// test提供时 表示哪些模块会被抽离
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
// 复用已经生成的chunk
reuseExistingChunk: true
}
}
}
}
};
还有一个很重要的配置是output.jsonpFunction
(默认是webpackJsonp
)。这是用于异步加载 chunk 的时候一个全局变量。如果多 webpack 环境下,为了防止该函数命名冲撞产生问题,最好设置成一个比较唯一的值。
一般而言,没有最完美的分包配置,只有最合适当前项目场景需求的配置。很多时候,默认配置已经足够可用了。
通常来说,为了保证 hash 的稳定性,建议:
1、使用webpack.HashedModuleIdsPlugin
。这个插件会根据模块的相对路径生成一个四位数的 hash 作为模块 id。默认情况下 webpack 是使用模块数字自增 id 来命名,当插入一个模块占用了一个 id(或者一个删去一个模块)时,后续所有的模块 id 都受到影响,导致模块 id 变化引起打包文件的 hash 变化。使用这个插件就能解决这个问题。
2、chunk
id 也是自增的,同样可能遇到模块 id 的问题。可以通过设置optimization.namedChunks
为 true(默认 dev 模式下为 true,prod 模式为 false),将chunk
的名字使用命名chunk
。
1、2 后的效果如下。
3、抽离 css 使用mini-css-extract-plugin
。hash 模式使用contenthash
。
这里以腾讯云某控制台页面以下为例,使用 webpack 路有异步加载效果后如下。可以看到,第一次访问页面。这里是先请求到一个总的入口 js,然后根据我们访问的路由(路由 1),再去加载这个路由相关的代码。这里可以看到我们异步加载的 js 数为 5,就相当于上面提到的默认配置项maxAsyncRequests
,通过waterfall
可以看到这里是并发请求的。如果再进去其它路由(路由 2)的话,只会加载一个其它路由的 js(或者还有当前没有加载过的 vendor js)。这里如果只修改了路由 1 的自己单独业务代码,vendor 相关的 hash 和其它路由的 hash 也不是不会变,这些文件就能很好的利用了浏览器缓存了
webpack 异步加载分包如何实现
我们知道,默认情况下,浏览器环境的 js 是不支持import
和异步import('xxx').