一次成功的Vue项目打包体积优化

224 阅读4分钟

问题背景

在交付项目的时候,发现在生产环境下,有一个JS文件经常加载失败,然后刷新一下又会加载成功。分析原因,发现生产环境的配置太低,而这个文件有4M,首次加载直接失败了导致页面白屏,因此我们需要优化一下打包的产物。

项目使用的技术框架是vben-admin,这是一个比较全面的后台管理系统的模板项目,最大的缺点就是太重量级了。

跟随这个大文件,我们去对项目做一下优化:

问题分析

  1. 多余的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,需要将其删除。复制其他项目的时候,经常会遇到这样的问题。

  1. 动态路由导致的文件遍历操作

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不支持动态路径,参数必须要是字面量(原因:在编译阶段就会处理)。

按照上面的修改方案,多余的引用关系将不会被生成,大大减小了文件体积。

  1. 分包策略

在分包之前,我们需要先分析一下,哪些包需要分。

首先使用分析器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 模块只有用到的页面才开始去加载,而不是打开登录页的时候就加载,提升了首次打开页面的性能。

需要把握好分包的颗粒度,颗粒度越精细,文件数量就越多。

总结

  1. 过度工程化,会让项目变得庞大,比较合适的方案是通过脚手架去定制功能。

  2. 针对一些全局性的改造和优化,有一个合适的规范非常重要,否则很难进行通用的优化。