深入WebPack code splitting
# 前言
在默认情况下,webpack会将所有的资源都打入
一个
文件内,这样一来如果我们的项目很大
,那就会导致最终生成的文件非常大
。会大大的加长请求时间
,导致用户体验不好。所以代码分割
是webpack中非常重要的一个优化手段。下面就一起来看看该如果去做吧
# SplitChunksPlugin
SplitChunksPlugin
是webpack4
开始自带的一个开箱即用的工具,他可以灵活的按需分割代码块
。
splitChunks: {
// 表示选择哪些 chunks 进行分割,可选值有:async,initial 和 all
chunks: "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 下可以可以配置多个组,每个组根据 test 设置条件,符合 test 条件的模块,就分配到该组。模块可以被多个组引用,但最终会根据 priority 来决定打包到哪个组中。默认将所有来自 node_modules 目录的模块打包至 vendors 组,将两个以上的 chunk 所共享的模块打包至 default 组。
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
// 一个模块可以属于多个缓存组。优化将优先考虑具有更高 priority(优先级)的缓存组。
priority: -10,
// 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
默认情况下,SplitChunks
只会对异步调用
的模块进行分割(chunks: "async"
),并且默认情况下处理的 chunk 至少
要有 20kb
,过小的模块不会被包含进去。
cacheGroups
缓存组是施行分割的重中之重
,他可以使用来自 splitChunks.*
的任何选项,但是 test
、priority
和 reuseExistingChunk
只能在缓存组级别上进行配置。默认配置中已经给我们提供了 Vendors
组和一个 defalut
组,**Vendors **组中使用 test: /[\/]node_modules[\/]/ 匹配了 node_modules 中的所有符合规则的模块。
Tip
当 webpack 处理文件路径时,它们始终包含 Unix
系统中的 /
和 Windows
系统中的 \
。这就是为什么在{cacheGroup}.test
字段中使用[\/]
来表示路径分隔符的原因。{cacheGroup}.test
中的 /
或 \
会在跨平台使用时产生问题。
例如我们想要把项目中使用的某个库拆分
出来,就可以这样配置
cacheGroups: {
react: {
name: 'react',
test: /[\\/]node_modules[\\/](react)/,
chunks: 'all',
priority: -5,
},
},
2
3
4
5
6
7
8
更多的配置项可以查看官网详细配置 (opens new window)
# 动态import
使用import()
方法,也可以将资源打入一个独立
的chunk
中,下面来看一下使用方法
// import { mul } from './test'
import $ from 'jquery'
import('./test').then(({ mul }) => {
console.log(mul(2,3))
})
console.log($)
// console.log(mul(2, 3))
2
3
4
5
6
7
8
9
10
ps:import
方法返回的是一个promise
,所以如果有用到里面的东西记得在then
回调中使用。
值得注意的是,这种语法还有一种很方便的`````“动态引用”```的方式,他可以加入一些适当的表达式,举个例子,假设我们需要加载适当的主题:
const themeType = getUserTheme();
import(`./themes/${themeType}`).then((module) => {
// do sth aboout theme
});
2
3
4
5
这样我们就可以“动态”加载我们需要的异步模块,实现的原理主要在于两点:
- 1.至少需要包含模块相关的路径信息,打包可以限定于一个特定的目录或文件集。
- 2.根据路径信息 webpack 在打包时会把 ./themes 中的所有文件打包进新的 chunk 中,以便需要时使用到。
# 魔术注释
有些小伙伴可能会问了,使用splitChunks
可以通过cache-group
配置项来指定分割出来的chunk
的名称,那通过import()
可不可以指定名称呢?答案是 当然可以,下面就来看看在webpack中如何给动态导入
的chunk指定名称吧
import(/* webpackChunkName: "my-chunk-name" */'./test')
2
只需要加上这样一段注释,webpack
就会为这个chunk
加上你自定义的名称
魔术注释不仅仅可以帮我们修改chunk
名这么简单,他还可以实现譬如预加载
等功能,这里举个例子:
我们通过希望在点击按钮时才加载我们需要的模块功能,代码可以这样:
// index.js
document.querySelector('#btn').onclick = function () {
import('./test').then(({ mul }) => {
console.log(mul(2, 3));
});
};
2
3
4
5
6
7
//test.js
function mul(a, b) {
return a * b;
}
console.log('test 被加载了');
export { mul };
2
3
4
5
6
7
在我们点击按钮的同时确实加载了test.js
的文件资源。但是如果这个模块是一个很大的模块,在点击时进行加载可能会造成长时间 loading 等用户体验不是很好的效果,这个时候我们可以使用我们的 /* webpackPrefetch: true */
方式进行预获取
,来看下效果:
// index,js
document.querySelector('#btn').onclick = function () {
import(/* webpackPrefetch: true */'./test').then(({ mul }) => {
console.log(mul(2, 3));
});
};
2
3
4
5
6
7
8
整个过程中,在画面初始加载的时候,test.js 的资源就已经被预先加载了
,而在我们点击按钮时,会从(prefetch cache)
中读取内容。这就是模块预获取
的过程。另外我们还有/* webpackPreload: true */
的方式进行预加载
。
但是 prefetch
和preload
听起来感觉差不多,实际上他们的加载时机
等是完全不同
的:
preload chunk
会在父 chunk
加载时,以并行
方式开始加载。prefetch chunk
会在父 chunk 加载结束后
开始加载。preload chunk
具有中等优先级
,并立即下载
。prefetch chunk
在浏览器闲置时下载
。preload chunk
会在父 chunk
中立即
请求,用于当下
时刻。prefetch chunk
会用于未来
的某个时刻。
# 总结
以上就是webpack中对代码分割
的基本实践,代码分割的主旨就是减少页面加载请求
的资源体积过大
导致体验不好的问题。当然也不要盲目的进行分割,因为代码分割虽然减少了文件体积,但同样的他也增加了文件的请求次数。所以一定要合理地进行优化,过度的以及不合适地优化可能会适得其反