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()
                        }
                    }
                }
            }
        }
相关推荐
qiyi.sky几秒前
JavaWeb——Vue组件库Element(3/6):常见组件:Dialog对话框、Form表单(介绍、使用、实际效果)
前端·javascript·vue.js
杨荧32 分钟前
【JAVA开源】基于Vue和SpringBoot的洗衣店订单管理系统
java·开发语言·vue.js·spring boot·spring cloud·开源
Front思1 小时前
vue使用高德地图
javascript·vue.js·ecmascript
feng_xiaoshi3 小时前
【云原生】云原生架构的反模式
云原生·架构
花花鱼4 小时前
@antv/x6 导出图片下载,或者导出图片为base64由后端去处理。
vue.js
流烟默4 小时前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
架构师吕师傅5 小时前
性能优化实战(三):缓存为王-面向缓存的设计
后端·微服务·架构
蒲公英10016 小时前
vue3学习:axios输入城市名称查询该城市天气
前端·vue.js·学习
团儿.7 小时前
解锁MySQL高可用新境界:深入探索MHA架构的无限魅力与实战部署
数据库·mysql·架构·mysql之mha架构
杨荧8 小时前
【JAVA开源】基于Vue和SpringBoot的旅游管理系统
java·vue.js·spring boot·spring cloud·开源·旅游