vue3 + element plus + vite 迁移实践

2,725 阅读8分钟

背景

最近入职了一家新公司,需要搭建一个后台管理系统,我想着这太简单啦,react antd pro一把梭。然后领导喜欢vue,说vue简单,写起来快...我说行嘛,刚好没写过vue3,那就全用最新技术,vue3、vite、element plus直接上。然而我还是想简单了,后端大哥选择了一个叫go-admin的框架,前端是vue2,element admin 那一套,说里面包含了用户系统的基础功能,权限神马的都有,前后端都不需要写这部分代码了。我...我说行嘛,做业务,效率优先,就用这个吧。然而我心里想的是,我自己按照这个架子用自己想用的技术搞一套出来不就行了,反正我有时间。

技术方案

当前的技术栈及相关依赖版本,vue 2.6.11,vuex 3.5.1,vue-router 3.4.7,vue cli 4.5.13,element ui 2.13.2

目标

  1. vue2.x 升级到vue 3.x
  2. element ui 升级到 element plus
  3. vue cli 升级到 vite

由于我是写react出身,所以有两种方案

  1. 使用react + antd 自己按当前项目的样子实现一套
  2. 基于当前项目渐进升级

方案一消耗的时间会比较长pass掉,那就直接采取方案二基于当前项目来。搜索引擎 xx一番,发现了这个gogocode Element UI 到 Element Plus 升级指南,唔,那就按照这个方法操作吧,切分支,弄。

实践

1. 精简项目

gogocode 会转换src目录下的代码,所以先对项目进行瘦身,删除无用的代码和组件,防止无关紧要的东西形成干扰,转换出现错误等。

2. GoGoCode 升级

按照 Element UI 到 Element Plus 升级指南 操作。

  • vue2 升级到vue3 gogocode -s ./src -t gogocode-plugin-vue -o ./src
  • 相关依赖升级 gogocode -s package.json -t gogocode-plugin-vue -o package.json
  • elemen ui升级 gogocode -s ./src -t gogocode-plugin-element -o ./src

3. 错误修复

经过以上两步操作,初步的升级完成啦,run dev跑一下,咔,命令行报错

  • Cannot set properties of undefined (setting 'preserveWhitespace')看一下,是vue.config.js 的一行代码报错options.compilerOptions.preserveWhitespace=trueemm,就是说compilerOptions是undefined,好说,默认值兜底,加一行options.compilerOptions=options.compilerOptions||{},错误成功解决。
  • 继续run dev,又报错了.. options has an unknown property 'overlay' 还是vue.config.js,dev Server里配置了这个属性,估计是新的vue cli不支持这个写法吧,查文档,原来是webpack5的问题,overlay的配置变啦,外面要包一层client...错误解决
  • 再run,又来错误webpack < 5 used to include polyfills for node.js core modules by default.好嘛,项目在处理路由的时候用到了path,根据报错提示,安装一下path-browserify,再在vue.config.js里加个别名配置,path: 'path-browserify',错误解决

再次run dev,OKOK,项目跑起来啦,额,样式怎么不太对,侧边栏和导航栏的样式都有点问题

预期效果

再点一点菜单,好家伙,压根不能跳转,挨个解决问题先

  • 右上角样式,没有垂直居中,改下class...很简单
  • 左侧头部应该是有两个菜单的,但是现在只展示了三个点... 仔细排查,看代码,没有啥问题,又看了下这个地方用的是el-menu组件,再查element-plus文档,终于发现玄机,el-menu 的ellipsis 属性默认为true,好吧,加上一行 :ellipsis = false 搞定
  • 再看菜单颜色,有一段是白色的,去看代码发现是这样的 :background-color="$store.state.settings.themeStyle === 'light' ? variables.menuLightBg : variables.menuBg"关键在variables 这个变量上,再一看 import variables from '@/styles/variables.scss';从scss导入进来的,打印一下发现variables 是个空对象,怪不得样式不对,再去看看vue cli文档,css modules的问题,把文件名改成variables.module.scss 就可以了,成功解决
  • 然后看路由跳转的问题,看了一下路由相关的代码,基础页面是静态路由,管理页面都是动态路由,菜单和路径都是接口返回的,动态路由都无法跳转,说明动态路由注册出现了问题,看一下代码 router.addRoutes(accessRoutes)vue-router官网查查,发现新版本没有addRoutes 方法了,只有addRoute,也就是说路由要一个一个添加。没有问题accessRoutes是个数组,遍历添加就好了
accessRoutes.forEach((item) => {
  router.addRoute('/', item);
});

好啦,这些错误修完,项目已经能正常运行啦,每个页面点一点,还是有一些小瑕疵,继续修。

4. 细节修复

  • 跳转页面的时候回出现个没有内容的error message,控制台又看不到报错,要解决。先找到哪里的代码弹出的message,页面跳转的时候出现,最有可能的就是路由相关的代码,查一查果然,路由守卫里写了 一段try catch代码,在catch里写了Message.error(error || 'Has Error');error是对象,这里的message就弹出了空,错误被吃掉了,控制台也看不到,那就打印一下吧。错误出现了 Catch all routes ("*") must now be defined using a param with a custom regexp. 就是说这个全匹配的路由出了问题,不能直接写* ,要写正则,找到代码 asyncRoutes.push({path:'*',redirect:'/',hidden:true})结合 vue router迁移文档,修改为 asyncRoutes.push({path:'/pathMatch(.*)*',redirect:'/',hidden:true}) 成功解决
  • 部分页面出现警告 Vue received a Component which was made a reactive object.意思说组件被搞成了响应式的,会浪费性能,看代码发现雀食,一些组件在模板里当做组件属性使用,都挂在了data上。解决方法很简单,我们升级了vue3呐,直接setup语法用起来。setup() {return {Components}}完美解决
  • scss 有警告 v-deep usage as a combinator has been deprecated. use deep( inner-selector ) instead深度选择器的问题,这个简单,直接按照提示改就好
  • 关于el-button,type = text需要修改为 text type = link需要修改为 link
  • 关于组件的size,只有 large、default、small三种,mini神马的都不行
  • 侧边菜单点一点,收起的时候,卡的一p,而且图标也不见了,在网上搜了搜,都是些没用的信息,移除动画效果呀,都不行,只能对着element plus 折叠菜单的demo看,终于发现了端倪,项目里的 menu-item 被div包裹了一层,而这个组件的样式选择器用的是 > 多了一层div就找不到了撒。根据div 的class 改下样式,okk。
  • vue router还有警告 can no longer be used directly inside or .哦,router-view和keep-live的嵌套顺序有问题,改改改
 <router-view v-slot="{ Component, route }">
    <transition name="fade-transform" mode="out-in">
      <keep-alive v-if="!route.meta.noCache" :include="cachedViews">
        <component :key="route.name" :is="Component" />
      </keep-alive>
      <component v-else :key="route.name" :is="Component" />
    </transition>
  </router-view>

这下好啦,错误和警告全部消除,vue3和element plus的升级完成,下一步需要把vue cli迁移到vite。

5. vue cli 升级到vite

网上搜索了一番,没有什么转换工具,那只能自己搞啦,首先用vite创建一个vue 3 + ts的工程,然后就照着扒就好啦。

  • 把vue cli相关的依赖删除掉
  • 按照刚刚生成好的vite 模板,安装一下vite相关的依赖,ps:这个时候可以直接上pnpm,安装依赖真的快
  • 创建vite.config.js,把内容先copy一下
  • package.json 的命令替换一下,都换成vite的

基本的东西搞好了,看看vue.config.js里用了哪些插件,先把影响功能的搞一搞,打包相关的不管,主要有两个插件

  • html plugin,就是可以在html里面用ejs语法插入变量的
  • svg plugin,svg雪碧图插件,高效复用svgicon

vite官方虽然没有,但是社区已经有了相关的插件,vite html pluginvite svg plugin。直接按照官网配置即可,另外还有一些插件element plus 自动导入、打包分析等等,我直接把配置文件放出来

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import Icons from 'unplugin-icons/vite';
import IconsResolver from 'unplugin-icons/resolver';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import Inspect from 'vite-plugin-inspect';
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
import { createHtmlPlugin } from 'vite-plugin-html';
import jsx from '@vitejs/plugin-vue-jsx';
import { visualizer } from 'rollup-plugin-visualizer';
import defaultSettings from './src/settings';

import { resolve } from 'path';

const autoprefixer = require('autoprefixer');

const name = defaultSettings.title || 'HT Mateverse'; // page title

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    jsx(),
    createHtmlPlugin({
      inject: {
        data: {
          name,
        },
      },
    }),
    Icons({
      autoInstall: true,
    }),
    Inspect(),
    AutoImport({
      imports: ['vue'],
      resolvers: [ElementPlusResolver(), IconsResolver()],
    }),
    Components({
      resolvers: [
        ElementPlusResolver(),
        IconsResolver({
          enabledCollections: ['ep'],
        }),
      ],
    }),
    createSvgIconsPlugin({
      iconDirs: [resolve(process.cwd(), 'src/icons/svg')],
      symbolId: 'icon-[name]',
      customDomId: '__svg__icons__dom__',
    }),
    visualizer(),
  ],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '#': resolve(__dirname, 'types'),
      path: 'path-browserify',
      'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js',
    },
  },
  css: {
    postcss: {
      plugins: [autoprefixer],
    },
  },
});

文件配置好啦,run个dev试试,果不其然报错

  • Failed to resolve import "./App" from "src\main.js".愣住,什么情况,文件目录没问题呀,怎么找不到,查查vite文档, resolve.extensions 配置,唔,省略的扩展名里没有.vue。加一下?然而官网里说(还是老老实实补全后缀吧):
  • 导入时想要省略的扩展名列表。注意, 建议忽略自定义导入类型的扩展名(例如:.vue),因为它会影响 IDE 和类型支持。
  • require is not defined,emm vite是es module 这套,require肯定是不行的,改一改,代码里require改成import,vite是支持glob 批量导入的,一些使用require的批量导入也都需要替换掉
  • process is not defined 代码里读取环境变量的时候又又报错了,再看看 vite 环境变量的文档,有两个点需要修改
    • 环境变量需要通过import.meta.env获取
    • 在env文件中声明的变量,需要以VITE 开头,否则是无法注册到项目中的

!!! 注意,主要的错误就这两个,require相关的代码需要先改好,否则会出现一些莫名奇妙的错误,把require的代码改了,自然就好了...

收官

run 个 dev,vite真的是名不虚传,10秒内启动项目。迁移和升级都完成了,在点一点吧,发现还要两个小问题

  1. vue-i18n 报了个警告, You are running the esm-bundler build of vue-i18n. It is recommended to configure your bundler to explicitly replace feature flag globals with boolean literals to get proper tree-shaking in the final bundle. 讲道理我没懂这玩意儿啥意思,一番搜索,在vite.config配置个别名,'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js'就好了。后面要仔细研究下
  2. 修改页面的js代码时,热更新报错 parentComponent.ctx.deactivate is not a function 刷新一下就好,修改模板代码也没问题..又去搜索了一下,很多都说在keep-alive下面的component上配置key,然而我试了试,并不行,最终在github上找到了此pr github.com/vuejs/core/…。是keep-alive相关的问题,pr还没合并,那我们怎么搞呢。很简单,keep-alive的热更新问题,生产环境是不需要热更新的,只需要判断下环境,开发环境不用keep-alive就好啦。
<template>
  <section class="app-main">
    <router-view v-slot="{ Component, route }">
      <transition name="fade-transform" mode="out-in">
        <keep-alive v-if="!route.meta.noCache && !hideKeepAlive" :include="cachedViews">
          <component :key="route.name" :is="Component" />
        </keep-alive>
        <component v-else :key="route.name" :is="Component" />
      </transition>
    </router-view>
  </section>
</template>

<script>
  export default {
    name: 'AppMain',
    setup() {
      return {
        // 解决开发环境下keep-alive导致的热更新问题
        hideKeepAlive: import.meta.env?.MODE === 'development',
      };
    },
</script>

完美,终于可以使用最新的vue技术栈啦,激动

总结

项目在精简后只有基本的用户信息和权限相关的功能,十几个页面,并不复杂。升级共花费了一周的时间,我的时间充裕,理论上肯定是可以压缩的。后续还有一些工作要做,生产环境打包的配置,包大小的优化。以及使用pinia替换vuex。

好啦,到此结束。