UmiJs - 拆包优化
- 前言
- 一. 如何拆包,怎么拆
- 1.1 分析自己项目的编译产物结构
- 1.2 开始拆包
- 二. 有哪些注意点
- 2.1 样式丢失
- 2.2 存在需单独打包的页面
前言
我们在写前端代码的时候,难以避免的是,我们可能引入的依赖越来越多。那么随之而来的,我们打包编译后的产物,也会随之越来越大。
默认情况下,使用Umi框架的人都知道,Umi
会将前端页面,凡是用到的依赖都给打包到umi.js
文件。同样地,CSS
相关的则打入umi.css
文件中。那当你的项目达到一定规模或者复杂程度上去的时候,尤其是umi.js
文件就会很大。因此本篇文章主要分享一下拆包的经验。
一. 如何拆包,怎么拆
拆包之前我们应该去了解自己本身的项目,打出来的js文件结构是怎样的。我们可以在package.json
文件中添加命令:
1.1 分析自己项目的编译产物结构
"scripts": {
"analyze": "cross-env ANALYZE=1 umi build",
}
然后执行如下命令:
npm run anaylze
最后umi
会分析你的编译产物,并弹出窗口进行图形化展示,如图:
可见我们的umi.js
文件,即便是压缩后,依旧有4.5M的大小。可见它不一般呐。
1.2 开始拆包
首先,umiJs
相关的配置,一般在目录的config/config.ts
文件中,官网文档
我们需要在配置里面添加配置项 chainWebpack
:
chainWebpack: (config) => {
config.optimization.splitChunks({
chunks: 'all',
minSize: 30000,
minChunks: 1,
maxSize: 0,
automaticNameDelimiter: '.',
name: true,
maxAsyncRequests: 30,
maxInitialRequests: 10,
cacheGroups: {
}
});
}
相关配置项的含义:
chunks
:用于指定哪些代码块应该被拆分。可以分为如下几种类型:
'all'
:所有代码块都会被拆分。'async'
:只拆分异步代码块。异步代码块是指通过import()
或类似方式动态加载的代码块。'initial'
:只拆分初始代码块。初始代码块是指在入口文件中直接引用的代码块。- 函数:可以使用一个函数来自定义哪些代码块应该被拆分。该函数接收一个参数
chunk
,代表当前的代码块。函数需要返回一个布尔值,表示该代码块是否应该被拆分。 (这个很有用)
其他的配置含义如下:
minSize
选项指定了拆分后的代码块最小大小。minChunks
选项指定了一个模块被多少个入口文件引用时才会被拆分。maxSize
选项指定了拆分后的代码块最大大小。automaticNameDelimiter
选项指定了拆分后的文件名的分隔符。name
选项指定了是否根据模块名自动生成文件名。maxAsyncRequests
选项指定了最大异步请求数。maxInitialRequests
选项指定了最大初始请求数。cacheGroups
选项用于配置缓存组。
大家可以照抄上面的配置。接下来看下cacheGroup
的写法:
cacheGroups: {
vendors: {
name: 'vendors',
test: /[\\/]node_modules[\\/]/,
priority: 11
},
reactFileViewer: {
name: 'reactFileViewer',
test: /(react-file-viewer)/,
priority: 12
},
}
我这里呢,将我的umiJs
中的相关依赖拆成了两个JS
文件 (umi.js
本身还是会存在的,只不过它里面大部分的内容被“转移”到这俩js文件中) 因为react-file-viewer
这个依赖它占用的大小非常非常的大,因此把他抽出来。
我们需要关注的是这么几个东西:
- 打包的文件名称:
name
。 - 打包的正则匹配:
test
。 - 优先级:
priority
属性用于设置优先级,数字越大,优先级越高,打包时会优先匹配优先级高的配置。 这个同样很重要!!!
上面的配置含义如下:
- 我优先把
react-file-viewer
这个依赖单独打包成reactFileViewer.js
文件。优先级12。 - 把剩余所有的依赖全部打入到
vendors.js
文件中。优先级11。因为我的正则写的是[\\/]node_modules[\\/]
,所以会把所有用到的依赖都打入进去。
那么最后再次执行对应的分析命令 npm run anaylze
,结果如下:
由于我这里是演示,其实umi.js
文件还是很大。那么你就需要看它里面到底装了什么东西,把比较大的东西给他拆出来,我总结下几个拆分的规则:
- 跟业务无关的,比如
antd
这种文件,可以单独拆出来。按照依赖的类型进行拆包,比如工具类相关的(lodash
、moment
等)、React
相关的等等。 - 特别特别大,但是只有个别页面使用的,可以单独抽出来,对应使用的页面引入对应
JS
,例如我上面的react-file-viewer
。 - 尽量做到均衡平分,因为页面加载
Js
是并行多个加载的,所以若你拆包不均衡,一个js
大小2M
,一个大小100KB
,那么最后页面加载的时间还是以最长的2M
为准。反之,如果你均分成两个1M
大小的文件,那么页面加载JS
的时长大概就在1秒。(都是粗略的计算)
二. 有哪些注意点
倘若你跟我一样,前端打包后,采取模板渲染的方式(比如Egg的ejs模板渲染),或者是外部显式引入umi.js
等静态资源文件的地方。那么你在拆包之后就需要考虑到资源的加载顺序问题了。
2.1 样式丢失
比如:倘若你把antd
相关的资源从umi.js
文件中隔离出来,那么你在外部引入资源的时候,就需要考虑到两件事:
antd
打出的包你也要引入,例如< link rel="stylesheet" href='antd.chunk.css' />
。- 特别重要:考虑到
antd
和umi
的文件加载顺序。例如:当你先引入umi.css
文件,再引入antd.css
文件(而umi.js
里面包含了我们的前端页面,里面使用到了antd
的组件),那么就会造成完成umi.js
文件加载的时刻,找不到对应的antd
样式,即样式丢失。
那么我们怎么办,建议:
- 由于我们把
umi.js
文件中大部分的依赖全部抽除去了,最终umi.js
文件的大小实际上只有几KB
,因此为了避免这种由于加载顺序导致的样式依赖丢失,我们可以把antd
这类资源保留到umi.js
中。
代码编写如下:
vendors: {
name: 'vendors',
test: /[\\/]node_modules[\\/](?!antd|@ant-design)/,
priority: 11
},
在原本的基础上,通过正则表达式,排除了antd
相关的依赖,即正则末尾加上:
(?!antd|@ant-design)
那么当umi.js
把剩余所有的依赖抽出到vendors
文件中的时候,会保留antd
相关资源,因此antd
相关的资源就会保留到umi.js
文件中。那么外部引入的时候,也不会有样式丢失的问题了。
2.2 存在需单独打包的页面
如果你的项目,已经把个别页面单独打包了,而某些情况下,你又不希望它打出来的文件被更改。那么你可以在拆包的过程中,将其排除:
比如,我的项目单独对一个页面打包,打包成了test.js
文件:
const ChunksIgnoreList = ['test','test2'];
chainWebpack: (config) => {
// 单独打包
config.entry('test').add('./src/pages/test/index.js');
config.entry('test2').add('./src/pages/test2/index.js');
// ...省略
config.optimization.splitChunks({
chunks: (chunk) => ChunksIgnoreList.filter((item) => item.includes(chunk.name)).length === 0,
// ...省略
});
}
我们就可以在splitChunks
中的chunks
属性,自定义函数(文章上面有提到,是可以写自定义函数的,我们就不要写all
了),这里将我们上面单独打包的文件,全部给排除掉了,只有chunk
的名称不是上面ChunksIgnoreList
中定义的几个,都允许被拆分打包。对应的拆包结果如下:
当然,这里需要补充一点:
- 如果你单独打包某个页面,又使用了
split
的拆包方式,那么是不建议前者的打包方式存在的。 - 比如
antd
为例,你单独打包一个页面,antd
可能会被打包到test.js
文件中。但是antd
同样又会被打包到umi.js
文件中,就造成了打包内容的重复。 - 因此推荐只用
split
方式打包就可以了。除非你有一些特殊的需求。