vue2 升级 vue3 迁移方案研究

1,328 阅读2分钟

目标

现有架构:vue2 + vue-cli

升级方向:vue3+vite

升级过程

旧代码用了 gogocode-cli 进行AST转换;

  1. vue部分

保留 vue2 options api 写法,自动转换了生命周期、双向绑定、emit事件、this.$set事件等

  1. Element

更新 icon引入、部分组件props更新等

相关资料导读:

vue:非兼容性更改

element:升级指南

gogocode-cli文献:AST代码

已知升级问题

1、Less 需手动修改为 Scss;

2、element icon 在组件中 import 引用,需手动去除;因项目已全局注入 element icon,直接写组件名即可,无需引入;

3、样式变量声明差异;less 是 @ 符,scss 是 $符 ;并且 lang 需改为 scss;

4、第三方插件依赖vue2版本的,需平替为vue3插件;

5、vue style 中,深度映射的 /deep/ 需改为 ::depp 或 :deep();

6、静态文件路径映射,文件夹层级修改,需同步修改;

7、路径别名 @xx 映射问题,目前只保留了 @ 映射;

8、项目 store 从 vuex 改为 pinia , vuex 的相关代码,需要改为 pinia ;

9、样式配色固定,需改为动态换肤的 var class

10、Vue3 移除了部分API,需修整代码

vue 升级

借助 gogocode-plugin-vue 做Vue代码转换

移除API(重点须知)

vue3 移除了部分API,重构时关注代码是否用了对应 API;

移除API导读

全局事件挂载

在 Vue2 中,全局事件挂载是通过 Vue的原型, Vue.prototype 来挂载;

而 Vue3 Vue.prototype 已经被删除,而且在组件内部也不再支持 this.$property 语法。Vue 通过 globalProperties 挂载全局事件;

// vue2
import Vue from 'vue'
Vue.prototype.$title = 'vue2';

// vue3
import { createApp } from 'vue';
const app = createApp(App);
app.config.globalProperties.$title= 'vue3';

双向绑定

在 Vue 2 中,使用 v-model 需要在组件中明确定义 prop 和事件。而在 Vue 3 中,组件的 v-model 默认行为被彻底重构了。

Vue 3中的 v-model绑定的 prop 为 modelValue,与Vue 2的 value有区别,需要注意这块代码;

现在,如果在组件上使用 v-model 指令,Vue 会自动推断出默认的 prop 和事件,并且将它们用于双向绑定。这意味着在许多情况下,你不再需要在组件中手动定义 prop 和事件。

// vue2 写法
<template>
    <your-component :title.sync="title"></your-component>
</template>

// vue3 写法
<template>
    <your-component v-model:title="title"></your-component>
</template>
// vue3绑定多个双向绑定
<template>
    <your-component v-model:title="title" v-model:name="name"></your-component>
</template>

插件转换把原先的 v-model 转换为 v-model:xxx形式;

Vue 3中的 v-model绑定的 prop 为 modelValue; element组件也遵循此规则;

这里是特殊的,但插件转换过程误判,转换错误,需要手动修正这些错误转换:

示例:

// 转换前
<el-input v-model="your-key"></el-input>
// 插件错误转换
<el-input v-model:value="your-key"></el-input>

// 正确写法,以下两种都可以
<el-input v-model="your-key"></el-input>
<el-input v-model:modelValue="your-key"></el-input>

Emit 声明

Vue3 需在 emits 数组里显性定义 emit 事件;而Vue2会自动注册;

// vue2 写法
<template>
    <button @click="$emit('A')">emit A</button>
    <button @click="emitB">emit B</button>
</template>

<script>
export default {
    methods: {
        emitB(){
            this.$emit('B');
        }
    }
}
</script>

// vue3 写法
<template>
    <button @click="$emit('A')">emit A</button>
    <button @click="emitB">emit B</button>
</template>

<script>
export default {
    methods: {
        emitB(){
            this.$emit('B');
        }
    },
    // 需显性声明 emit 事件
    emits:[ 'A', 'B']
}
</script>

代码转换后,会自动注入一个 utils;该utils用不上,需手动删除:

// 需把以下代码删除
import { $on, $off, $once, $emit } from '项目路径/utils/gogocodeTransfer'

插槽更新

vue3 的书写形式与 vue2 有差别,插件已做了同步转换:

// vue2
<template slot-scope="scope"></template>
<template slot="empty"></template>

// 插件转换后,vue3
<template v-slot="scope"></template>
<template v-slot:empty></template>

生命周期更新

Vue 3 中,一些钩子函数已经被移除,如 beforeCreatebeforeMount。同时,也新增了一些钩子函数,如 onBeforeMountonUnmounted。具体变化如下:

Vue 2Vue 3
beforeCreatesetup
beforeMount-
beforeUpdate-
beforeDestroyunmounted
activatedonActivated
deactivatedonDeactivated

Filter 相关

template 中使用了 filter 指令的代码替换丢失,使用到指令的模板,需排查转换是否成功

template模板

代码书写不规范;

Template 作为 具名插槽,其父节点必须是个元素。例子如下:

// 错误写法
<tempalte v-if="true">
    <tempalte v-slot="xxx"> </tempalte>
</tempalte>

// 正确
<tempalte v-if="true" v-slot="xxx"> </tempalte>

$set

Vue 3 底层使用了 proxy 做双向绑定更新,解决了 Vue 2 双向绑定深层更新失败问题

更新数组对象无需使用 $set

Element ui 升级

Icon 引用

原有的 icon class 变成 对应icon组件;切部分 icon 命名有所变动;

请查阅 icon图标

// 旧 elementui
<i class="el-icon-download"></i>

// 新 elementPlus
<el-icon><DownLoad /></el-icon>

Vite 已在全局自动注册 el-icon;用 gogocode-element-cli 转换的时候,出现系列问题,如下:

  1. icon 自动会引入插件库有误;自动引入改为 @element-plus/icons 应修改为 @element-plus/icons-vue

  2. 挂载 icon-class 的元素,且挂载了事件,事件会被清除

组件props变更

部分组件 props 变更;具体变更,请查阅官方文档

若在测试过程中发现问题,排查过程中,可配合查看组件API;

已知变更组件:

  1. Button

  2. Date pickter

  3. Dialog

  4. Icon

  5. Popover

插槽变更

  1. slot-scope="scope" 更新为 v-slot="scope"

  2. slot="插槽名" 更新为 v-slot:插槽名

样式替换

Style lang 声明

预处理器从 Less 改为 Sass

// less
<style lang="scss" scoped></style>

// sass
<style lang="scss" scoped></style>

深度选择器

深度选择器 /deep/ 改为 ::deep:deep(your-class)

// vue2
<style>
  /deep/ .el-loading {}
</style>

// vue3,有2种写法
<style>
  ::deep .el-loading {}
  :deep(.el-loading) {}
</style>

变量声明

变量声明,将 @class 替换为 $class

// less
<style lang="scss" scoped>
    @color: red;
</style>

// sass
<style lang="scss" scoped>
    $color: red;
</style>

样式继承

样式继承需新增关键符 @extend

.extend-class {
 ....
}
// less写法
.title {
 // 继承样式
 .extend-class;
}

// scss写法
.title {
 // 继承样式
 @extend .extend-class;
}

换肤

新框架引入换肤方案,需把固定的颜色都改成全局的 var css;包括:

字体颜色、背景色、边框色、hover、高亮颜色等

Vuex 替换为 Pinia

Vuex 与 Pinia 使用方式上大抵是类似的,但是在方法的注册以及引用上,仍有区别

文档参考

Vuex 与 Pinia 差异

  1. pinia需要使用 defineStore 注册 stroe;而 vuex不需要;
  2. pinia移除mutations对象
  3. pinia需要添加一个必要的 id
  4. actions中的方法,第一个参数的上下文去除,直接使用this代替
  5. pinia初始化数据可以使用 this.$reset
  6. pinia更新数据;
  • 在store里,直接使用 this.xx更新
  • 组件引用
import useAuthUserStore from '@/store/useAuthUserStore';
const useAuthUserStore  = useAuthUserStore();
// 更新 firstName、lastName
useAuthUserStore.$patch({
    firstName: "",
    lastName: ""
})
// 也可以用函数的形式
useAuthUserStore.$patch((state)=> {
    state.firstName = "";
    state.lastName = "";
})
// vuex
const storeModule = {
  namespaced: true,
  state: {
    firstName: '',
    lastName: '',
    userId: null
  },
  getters: {
    firstName: (state) => state.firstName,
    fullName: (state) => `${state.firstName} ${state.lastName}`,
    loggedIn: (state) => state.userId !== null,
    // 与其他模块的一些状态相结合
    fullUserDetails: (state, getters, rootState, rootGetters) => {
      return {
        ...state,
        fullName: getters.fullName,
        // 读取另一个名为 `auth` 模块的 state
        ...rootState.auth.preferences,
        // 读取嵌套于 `auth` 模块的 `email` 模块的 getter
        ...rootGetters['auth/email'].details
      }
    }
  },
  actions: {
    async loadUser ({ state, commit }, id: number) {
      if (state.userId !== null) throw new Error('Already logged in')
      const res = await api.user.load(id)
      commit('updateUser', res)
    }
  },
  mutations: {
    updateUser (state, payload) {
      state.firstName = payload.firstName
      state.lastName = payload.lastName
      state.userId = payload.userId
    },
    clearUser (state) {
      state.firstName = ''
      state.lastName = ''
      state.userId = null
    }
  }
}
export default storeModule;

// pinia
import { defineStore } from 'pinia'
export default useAuthUserStore = defineStore('auth/user', {
  // 转换为函数
  state: (): State => ({
    firstName: '',
    lastName: '',
    userId: null
  }),
  getters: {
    // 不在需要 firstName getter,移除
    fullName: (state) => `${state.firstName} ${state.lastName}`,
    loggedIn: (state) => state.userId !== null,
    // 由于使用了 `this`,必须定义一个返回类型
    fullUserDetails (state){
      // 导入其他 store
      const authPreferencesStore = useAuthPreferencesStore()
      const authEmailStore = useAuthEmailStore()
      return {
        ...state,
        // `this` 上的其他 getter
        fullName: this.fullName,
        ...authPreferencesStore.$state,
        ...authEmailStore.details
      }
      // 如果其他模块仍在 Vuex 中,可替代为
      // return {
      //   ...state,
      //   fullName: this.fullName,
      //   ...vuexStore.state.auth.preferences,
      //   ...vuexStore.getters['auth/email'].details
      // }
    }
  },
  actions: {
    //没有作为第一个参数的上下文,用 `this` 代替
    async loadUser (id: number) {
      if (this.userId !== null) throw new Error('Already logged in')
      const res = await api.user.load(id)
      this.updateUser(res)
    },
    // mutation 现在可以成为 action 了,不再用 `state` 作为第一个参数,而是用 `this`。
    updateUser (payload) {
      this.firstName = payload.firstName
      this.lastName = payload.lastName
      this.userId = payload.userId
    },
    // 使用 `$reset` 可以轻松重置 state
    clearUser () {
      this.$reset()
    }
  }
})
export default storeModule;

参考文献

升级 Vue3 的最后一块拼图

Vue 2 升级 Vue 3

Vue2升级Vue3必看全面对照迁移示例