目标
现有架构:vue2 + vue-cli
升级方向:vue3+vite
升级过程
旧代码用了 gogocode-cli 进行AST转换;
- vue部分
保留 vue2 options api 写法,自动转换了生命周期、双向绑定、emit事件、this.$set事件等
- 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;
全局事件挂载
在 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 中,一些钩子函数已经被移除,如 beforeCreate 和 beforeMount。同时,也新增了一些钩子函数,如 onBeforeMount 和 onUnmounted。具体变化如下:
| Vue 2 | Vue 3 |
|---|---|
| beforeCreate | setup |
| beforeMount | - |
| beforeUpdate | - |
| beforeDestroy | unmounted |
| activated | onActivated |
| deactivated | onDeactivated |
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 转换的时候,出现系列问题,如下:
-
icon 自动会引入插件库有误;自动引入改为
@element-plus/icons应修改为@element-plus/icons-vue -
挂载 icon-class 的元素,且挂载了事件,事件会被清除
组件props变更
部分组件 props 变更;具体变更,请查阅官方文档;
若在测试过程中发现问题,排查过程中,可配合查看组件API;
已知变更组件:
-
Button
-
Date pickter
-
Dialog
-
Icon
-
Popover
插槽变更
-
slot-scope="scope" 更新为 v-slot="scope"
-
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 差异
pinia需要使用defineStore注册 stroe;而 vuex不需要;pinia移除mutations对象pinia需要添加一个必要的idactions中的方法,第一个参数的上下文去除,直接使用this代替pinia初始化数据可以使用this.$resetpinia更新数据;
- 在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;