Vue CLI到Vite:升级你的Vue项目

背景

VUE CLI 项目是基于 webpack 构建,随着业务代码越来越多,构建速度会越来越慢,包体积也会很大,而且 VUE CLI 逐渐不再维护了。而 Vite 构建工具在市场上越来越成熟,已经升级到了 Vite 4.0 版本,市场上的响应也比较好,新项目基本上使用 Vite 来搭建,旧项目升级 Vite 也有了很多的成功案例,为了让旧的项目也具有这种好的开发体验,统一技术栈,所以有必要升级 Vue-Cli 为 Vite,提高项目的开发效率

升级方案

项目比较小,可以将开发环境和生产环境打包都使用 Vite 打包

如果项目体积和业务比较大,一下子开发和打包升级,会带来一定的风险,所以采用渐进式升级

  1. 开发环境使用 Vite 构建,仍保留 webpack 打包功能,先享受到 Vite 带来的友好开发体验
  2. 在开发环境和测试打包稳定后,再替换使用 Vite 打包生产代码

升级实现步骤

安装必要依赖

项目使用 vue3,如果要支持 jsx,先安装下面三个依赖

yaml 复制代码
npm i vite @vitejs/plugin-vue @vitejs/plugin-vue-jsx -D

新增 vite.config.js 配置

Vite 的默认执行配置文件是 vite.config.js,插件 plugins 是一个数组,插件安装是一个函数,直接调用执行

jsx 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'

export default defineConfig({
    plugins: [
        vue(),
        vueJsx()
    ]
})

创建 html 文件

Vite 默认会先加载 html 文件,然后根据入口文件,请求 js 文件,html 文件默认是在项目根目录下

推荐使用 vite 插件 vite-html-plugin 生成 html,该插件具有功能

  • HTML 压缩能力
  • EJS 模版能力
  • 多页应用支持
  • 支持自定义entry
  • 支持自定义template

根据模板+数据注入生成html

jsx 复制代码
import { defineConfig } from 'vite'
import { createHtmlPlugin } from 'vite-plugin-html'

export default defineConfig({
  plugins: [
    createHtmlPlugin({
      minify: true,
      /**
       * 在这里写entry后,你将不需要在`index.html`内添加 script 标签,原有标签需要删除
       * @default src/main.ts
       */
      entry: '/src/main.ts',
      /**
       * 如果你想将 `index.html`存放在指定文件夹,可以修改它,否则不需要配置
       * @default index.html
       */
      template: 'index.html',

      /**
       * 需要注入 index.html ejs 模版的数据
       */
      inject: {
        data: {
          title: 'index',   // 出现在模版中的 <%- title %>
          injectScript: `<script src="./inject.js"></script>`, // 出现在模版中的<%- injectScript %>
        },
        tags: [
          {
            injectTo: 'body-prepend',
            tag: 'div',
            attrs: {
              id: 'tag',
            },
          },
        ],
      },
    }),
  ],
})

html 文件

html 复制代码
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="/favicon.ico">
    <title><%- title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%- title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
</body>
</html>

兼容webpack和vite的环境变量用法

vue-cli 环境变量使用 process.env,然而 vite 不能识别 process node 模块,解决办法使用vite插件 vite-plugin-env-compatible ,让vite可以使用webpack中读取环境变量的方式

yaml 复制代码
process.env.VUE_APP_BASE_API

使用vite插件 vite-plugin-env-compatible ,让vite中可以使用webpack中读取环境变量的方式

jsx 复制代码
import { defineConfig } from 'vite'
import envCompatible from 'vite-plugin-env-compatible'

export default defineConfig({
  plugins: [
    envCompatible()
  ]
})

兼容 commonjs 代码

Vite 使用 ESM 模块加载,项目中有用到commonjs规范的依赖,需要进行转换,解决办法使用 vite 插件 @originjs/vite-plugin-commonjs

jsx 复制代码
// vite.config.js  忽略其他代码
import { viteCommonjs } from '@originjs/vite-plugin-commonjs'

export default ({ mode }) => {
    return defineConfig({
        plugins: [
            // ...
            viteCommonjs() // 兼容vite中的cjs导入语法
        ]
    })
})

如果有 es 模块,优先使用 es 包,如 lodash 模块 使用 lodash-es

解决css预处理的问题

1、样式路径引用问题,在 vue-cli 样式绝对路径是通过 ~@ 方式引用,在 vite 会报错

jsx 复制代码
<style lang="scss" scoped>
@import '~@/styles/variables';

...
</style>

解决办法,使用 resolve.alias 路径别名

js 复制代码
resolve: {
        alias: [
            {
                find: '@',
                replacement: path.resolve(__dirname, 'src')
            },
            {
                find: '~@',
                replacement: path.resolve(__dirname, 'src')
            }
        ],
    },

2、引用全局 SCSS变量

SCSS 变量为了避免在多个文件重复引入,在 vite 配置 scss 预处理器

jsx 复制代码
import { defineConfig, normalizePath } from 'vite'

// 全局 scss 文件的路径
// 用 normalizePath 解决 window 下的路径问题
const variablePath = normalizePath(path.resolve('./src/styles/variables.scss'))

export default defineConfig({
  // css 相关的配置
    css: {
        preprocessorOptions: {
            scss: {
                // additionalData 的内容会在每个 scss 文件的开头自动注入
                additionalData: `@import "${variablePath}";`
            }
        }
    }
})

variables.scss 文件,如果想让 js 也可以引用 scss 变量,会通过下面方式导出

scss 复制代码
$mainColor: #2f54eb;
// 省略
:export {
    mainColor: $mainColor;
    ...
}

在其他文件如 js 中引入相关变量

jsx 复制代码
import variables from '@/styles/variables.scss'
console.log(variables.mainColor) // #2f54eb

上面在 vue-cli 是正常的,在 vite 会报错,表示已经在全局引入,原因是重复引入了

解决办法是,新建一个variables.module.scss的文件,把 scss 变量定义在这个文件里,然后 variables.scss 引入这个变量文件

jsx 复制代码
@import './variables.module.scss';

// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
// JS 与 scss 共享变量,在 scss 中通过 :export 进行导出,在 js 中可通过 ESM 进行导入
:export {
    menuText: $menuText;
    ...
}

修改 vite 中引入的样式文件 variables.scssvariables.module.scss ,这样可以避免冲突了

jsx 复制代码
const variablePath = normalizePath(path.resolve('./src/styles/variables.module.scss'))

node 内置模块不兼容

vite 不支持 fs , path 内置模块,下载 path-browserify ,修改引入方法

js 复制代码
mport path from 'path'   // 修改

import path from 'path-browserify'

require.context 自动注册 svg 报错

require.context 是 webpack API,vite 不支持 require commonjs 规范,使用 createSvgIconsPlugin 插件解决

1、安装 createSvgIconsPlugin vite 插件

jsx 复制代码
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'

export default defineConfig({
    plugins:[
        createSvgIconsPlugin({
          // 指定需要缓存的图标文件夹(路径为存放所有svg图标的文件夹不单个svg图标)
          iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')],
          // 指定symbolId格式
          symbolId: 'icon-[dir]-[name]'
        })
    ]
})

• 在入口文件 main.ts 中引入

jsx 复制代码
import 'virtual:svg-icons-register'

需要注意是使用 svg 名称的拼接格式 symbolId: 'icon-[dir]-[name]' ,它是 icon + 目录 + 文件名

jsx 复制代码
# src/icons
- icon1.svg # icon-icon1
- icon2.svg # icon-icon2
- icon3.svg # icon-icon3
- dir/icon1.svg # icon-dir-icon1
- dir/dir2/icon1.svg # icon-dir-dir2-icon1

可以测试,打印所有的 svg 组件

jsx 复制代码
import ids from 'virtual:svg-icons-names'
// => ['icon-icon1','icon-icon2','icon-icon3']

server 代理配置

jsx 复制代码
server: {
        strictPort: false,
        port: 8877, // 启动端口
        open: true, // 自动打开浏览器
        proxy: {
            '/API': {
                target: 'http://XXX',
                changeOrigin: true
            }
        }
    }

循环引用报错

如果项目引入不规范,会经常遇见 Cannot access '...' before initialization 报错提示,大概率是循环引用导致的问题,这时就要去代码里检查修改为正确的引入方式

有个 issue 讨论过这个问题

生产打包

包分析可视化工具

安装工具 rollup-plugin-visualizer

jsx 复制代码
import { visualizer } from "rollup-plugin-visualizer";

import { defineConfig } from 'vite'
export default defineConfig({
  plugins: [visualizer()]
})

图片资源打包

打包压缩图片资源,使用 vite-plugin-imagemin

yaml 复制代码
npm i vite-plugin-imagemin -D
jsx 复制代码
viteImagemin({
            gifsicle: {
                optimizationLevel: 7,
                interlaced: false
            },
            optipng: {
                optimizationLevel: 7
            },
            mozjpeg: {
                quality: 20
            },
            pngquant: {
                quality: [0.8, 0.9],
                speed: 4
            },
            svgo: {
                plugins: [
                    {
                        name: 'removeViewBox'
                    },
                    {
                        name: 'removeEmptyAttrs',
                        active: false
                    }
                ]
            }
        }),

algorithm 指定了压缩算法为 gzip

threshold 指定文件大于10240b(10kb)时才压缩文件;

verbose 禁止在控制台输出压缩结果;

deleteOriginFile 指定压缩完文件后删除源文件

压缩文件

压缩打包后的资源文件

jsx 复制代码
npm i vite-plugin-compression -D
jsx 复制代码
import { defineConfig } from 'vite
import viteCompression from 'vite-plugin-compression'
export default defineConfig({
  plugins:[
    viteCompression(
      {
        algorithm: 'gzip',
        threshold: 10240,
        verbose: false,
        deleteOriginFile: true
      }
    )
  ]
})

分包策略

如何要减少包体积,配置代码分割,以 element-plus 拆包为例

jsx 复制代码
build: {
            outDir: 'dist',
            chunkSizeWarningLimit: 500,
            rollupOptions: {
                output: {
                    manualChunks (id) {
                        if (id.includes('element-plus')) {
                            return // 不打入这个文件相关的东西
                        }

                        if (id.includes('node_modules')) {
                            return id.toString().split('node_modules/')[1].split('/')[0].toString()
                        }
                    }
                }
            }
        }
相关推荐
用户51681661458411 天前
Vue Router 路由懒加载引发的生产页面白屏问题
vue.js·vue-router
前端缘梦1 天前
Vue Keep-Alive 组件详解:优化性能与保留组件状态的终极指南
前端·vue.js·面试
Simon_He1 天前
这次来点狠的:用 Vue 3 把 AI 的“碎片 Markdown”渲染得又快又稳(Monaco 实时更新 + Mermaid 渐进绘图)
前端·vue.js·markdown
稻草人22221 天前
java Excel 导出 ,如何实现八倍效率优化,以及代码分层,方法封装
后端·架构
数据智能老司机1 天前
精通 Python 设计模式——创建型设计模式
python·设计模式·架构
数据智能老司机1 天前
精通 Python 设计模式——SOLID 原则
python·设计模式·架构
王同学QaQ1 天前
Vue3对接UE,通过MQTT完成通讯
javascript·vue.js
华仔啊1 天前
基于 RuoYi-Vue 轻松实现单用户登录功能,亲测有效
java·vue.js·后端
艾小码1 天前
告别Vue混入的坑!Composition API让我效率翻倍的3个秘密
前端·javascript·vue.js
bobz9651 天前
k8s svc 实现的技术演化:iptables --> ipvs --> cilium
架构