问题背景
在交付项目的时候,发现在生产环境下,有一个JS文件经常加载失败,然后刷新一下又会加载成功。分析原因,发现生产环境的配置太低,而这个文件有4M,首次加载直接失败了导致页面白屏,因此我们需要优化一下打包的产物。
项目使用的技术框架是vben-admin,这是一个比较全面的后台管理系统的模板项目,最大的缺点就是太重量级了。
跟随这个大文件,我们去对项目做一下优化:
问题分析
- 多余的SVG引用
svgSprite.ts的写法,把src/assets/icons目录下所有的svg图片都加载了;
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
import path from 'path';
export function configSvgIconsPlugin(isBuild: boolean) {
const svgIconsPlugin = createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
svgoOptions: isBuild,
// default
symbolId: 'icon-[dir]-[name]',
});
return svgIconsPlugin;
}
这种插件加载SVG的方法并没有实现按需打包,而是将其作为一个通用SVG去加载,因此在这个目录下,我们不能把业务的SVG放进去。
优化方案:这个目录下,如果有无用的svg,需要将其删除。复制其他项目的时候,经常会遇到这样的问题。
- 动态路由导致的文件遍历操作
Vben-Admin有一个动态路由模式,我们实际项目也确实使用了这种模式,其原理就在于提前建立好全量的路由和Vue文件的关系,然后去匹配,代码如下:
import.meta.glob('../../views/**/*.{vue,tsx}');
这样的代码,打包的产物就是这样子的:
[{
'../../views/XXX/index.vue': () => e.import('./index-legacy.xxx.js'),
},
{
'../../views/XXXX/index.vue': () => e.import('./index-legacy.xxxx.js')
}]
遍历了所有符合条件的文件,然后对每一个文件都进行处理,得到一个文件路径和js文件的对应关系。
这里的问题就在于:并不是所有的.vue、.jxs文件都是路由的入口,我们实际上只需要处理作为路由入口的文件即可。
这里有两种解决方案:
一种是制定好代码规范,例如入口文件都叫index.vue,其他文件都不允许叫index.vue,这样我们就可以直接这样写:
import.meta.glob('../../views/**/index.vue');
另一种方案就是把所有的文件引用全写到本地文件中,这样非入口文件不会生成对应关系了。
export const routerList = [
import.meta.glob('../../views/system/role/index.vue'),
import.meta.glob('../../views/system/account/index.vue'),
import.meta.glob('../../views/system/notice/index.vue'),
import.meta.glob('../../views/system/menu/index.vue'),
]
let obj = {}
routerList.forEach(item => {
let key = Object.keys(item)[0]
let value = item[key]
obj[key] = value
})
export const routerObj = obj
这里没有办法做到服务端控制加载哪些文件,因为import.meta.glob不支持动态路径,参数必须要是字面量(原因:在编译阶段就会处理)。
按照上面的修改方案,多余的引用关系将不会被生成,大大减小了文件体积。
- 分包策略
在分包之前,我们需要先分析一下,哪些包需要分。
首先使用分析器rollup-plugin-visualizer看一下打包后的项目情况。
配置方式:
import visualizer from 'rollup-plugin-visualizer';
export function isReportMode(): boolean {
return process.env.REPORT === 'true';
}
export function configVisualizerConfig() {
if (isReportMode()) {
return visualizer({
filename: 'visualizer/stats.html',
open: true,
gzipSize: true,
brotliSize: true,
}) as Plugin;
}
return [];
}
{
"report": "cross-env REPORT=true npm run build",
}
执行完npm run report之后,生成了一个html,打开后就能直观看到项目的分包情况。
这样我们就可以配置一下分包策略,把一些模块拆分出来,如下所示:
{
rollupOptions: {
output: {
manualChunks: {
lodash: ['lodash-es', 'dayjs', 'clipboard', 'vue-i18n']
echarts: ['echarts', 'element-resize-detector'],
'vue-vendor': ['vue', 'vue-router', 'vuedraggable', 'pinia', 'vue-json-pretty', 'vue-uuid', 'vue-types'],
library: ['ant-design-vue', 'nprogress'],
richtext: ['tinymce', 'vditor', '@tinymce/tinymce-vue'],
'icon-resource': ['@iconify/iconify', '@ant-design/icons-vue', '@iconify/json', '@purge-icons/generated'],
comp: ['src/store/modules/permission.ts']
}
}
}
}
配置一下分包策略,这个可以根据项目情况自己去指定。我这边把项目用到的方法库、echarts、vue组件库、富文本、图标单独分出来,最终都会各自生成一个打包产物。comp这个就是动态路由相关的逻辑,也将其单独拿出来。
最终得到了打包后的产物如下:最大的文件只有 1M 左右,且某些文件会在需要的时候进行加载,例如 echarts 模块只有用到的页面才开始去加载,而不是打开登录页的时候就加载,提升了首次打开页面的性能。
需要把握好分包的颗粒度,颗粒度越精细,文件数量就越多。
总结
-
过度工程化,会让项目变得庞大,比较合适的方案是通过脚手架去定制功能。
-
针对一些全局性的改造和优化,有一个合适的规范非常重要,否则很难进行通用的优化。