项目框架由vue2.6升级到vue3.0,总结下升级过程中的遇到的问题及解决方法。
(本文只讨论vue3.0非兼容性更新的升级处理方式,对于composition api可翻阅我的其他文章)
一、项目简介
基于vue2的多项目聚合方案,分为多个子项目,主要运行在hybrid app中,之前也写过该项目最初搭建的教程:传送门。
不过原项目刚搭建时是js版本,升级vue3.0之前项目已经升为ts版了(见vue2升级ts教程),所以这次就只升vue框架就行,如果你的项目还没ts化,建议先升级完vue3.0再考虑升ts,一步一步来,因为我升级vue3.0过程中被很多ts校验问题搞得头大,几次都快要放弃,略艰辛。。。
再说下目前项目的体量:
- 路由页面,总共54个
- 页面+组件,共计140个
- 项目严格配置了eslint+stylelint
配置了代码校验,对于升级vue3.0还是很有帮助的,虽然校验严格,修改点会多很多,但也较大限度提升了代码稳定性,能避免很多升级带来的bug隐患。
全部升级完成历时大概三四天,前面两天主要在解决各种问题把项目单个页面跑通,跑通后基本就轻松了,后面就是重复的修改适配页面。
二、迁移基础
本身原项目就是vue-cli生成的,所以这次也是,先用vue-cli生成个vue3.0的项目,参考:vue-cli官方文档。
生成完后,就是对比原项目和新生成的vue3.0项目文件,对照着更改。
1、更新依赖
首先就是对比package.json
文件,更新依赖。
我这里涉及更新的一些依赖有:
- vue及vue-cli基础:
"dependencies": {
"vant": "^3.0.0-rc.2",
"vue": "^3.0.4",
"vue-router": "^4.0.1",
"vuex": "^4.0.0-rc.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
},
- eslint相关:
"devDependencies": {
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/eslint-config-standard": "^5.1.2",
"eslint-plugin-vue": "^7.0.0-0",
},
- ts相关:
"devDependencies": {
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/eslint-config-typescript": "^5.0.2",
},
Tips:
vue2.x的vue-template-compiler
移除了,新加了@vue/compiler-sfc
。
单元测试相关的依赖也有修改,这里就不再说了。
三、迁移点
首先附上一些官方文档:
vue3.0官方中文文档
vue3.0官方迁移指南
vue-router4.0官方升级指引
vuex官方升级指引
vant3官方升级指引
升级过程基本就是解决报错、翻阅文档的过程,偶尔百度谷歌,不过由于vue3.0发布没多长时间,目前大多数问题网上都搜不到,主要还是靠自己。
我项目用到的ui框架是vant,已经支持vue3.0了,个人建议,有些棘手的问题不妨翻阅下vant的源码作参考。
vue3.0引入了composition api,也就是setup的写法,但是老的写法还是兼容支持的,升级老项目不建议再改成setup方式,因为改动太大容易出bug,没必要,新写页面再用setup方式就行了。
以下是我项目升级中遇到的修改点(有些不兼容更新没有列举,建议翻阅官方迁移指南):
1、根组件App.vue
vue2.x里在html模板和根组件App.vue里都定义了一个<div id="app"></div>
的标签,实际打包后的dom结构只会保留一个id="app"的div。
而在vue3.0里,如果还这样写,打包后就生成了两层id="app"的div了,所以需要把App.vue里的那层移除。
// vue 2.x
<template>
<div id="app">
<router-view/>
</div>
</template>
// vue 3.0
<template>
<router-view/>
</template>
2、全局api的注册
vue2.x是通过在vue原型上添加属性或方法来注册全局api。
而vue3.0通过配置app实例来注册,避免原型污染,也便于ts校验。
// vue 2.x
Vue.prototype.$http = () => {}
// vue 3.0(注意此app必须是入口文件main.ts里生成的app实例)
app.config.globalProperties.$http = () => {}
3、v-model 和 .sync修饰符
:propname.sync
修改为 v-model:propname
<!-- vue 2.x -->
<ruleModal :isShow.sync="show" />
<!-- vue 3.0 -->
<ruleModal v-model:isShow="show" />
v-model绑定子组件的默认属性和事件名变了:
prop:value -> modelValue
;
event:input -> update:modelValue
;
4、Vue.extend
在vue2.x中,通过Vue.extend可以继承一个组件生成一个新的组件实例,在封装组件时很有用,比如loading组件。而vue3.0移除了这一api,vue3.0里只能使用createApp来生成vue实例,使用方式完全不同,所以这一块改动会比较大。
我的解决方式是通过createApp配合jsx语法来创建实例。
以封装loading加载效果为例:
// vue 2.x
import LoadingComp from './index.vue'
const ele = document.createElement('div')
// 挂载loading组件
const LoadingPlugin = Vue.extend(LoadingComp)
const loadingInstance = new LoadingPlugin({
el: ele
})
document.body.appendChild(loadingInstance.$el)
// 显示loading组件
loadingInstance.isShow = true
// vue 3.0
import LoadingComp from './index.vue'
import { createApp } from 'vue'
const ele = document.createElement('div')
document.body.appendChild(ele)
// 挂载loading组件
const myApp = createApp({
data () {
return {
isShow: false
}
},
render () {
return (
<LoadingComp isShow={this.isShow}></LoadingComp>
)
}
})
const loadingInstance = myApp.mount(ele)
// 显示loading组件
loadingInstance.isShow = true
这样就可以通过改变loadingInstance.isShow的值来控制loading组件的显示隐藏。
Tips:
- vant里toast等组件是通过类似
myApp.mount(ele)
和myApp.unmount(ele)
来控制组件的显示隐藏,目前不建议这样做,实测在created里直接调用隐藏组件功能时会有警告,导致mounted
和beforeUnmount
钩子不执行,暂不清楚是不是vue的bug。
5、生命周期钩子
组件和自定义指令的生命周期钩子函数都统一了。
自定义指令:
- bind → beforeMount
- inserted → mounted
- update 已移除,可以用updated替代
- componentUpdated → updated
- unbind -> unmounted
组件:
- update → updated
- beforeDestroy → beforeUnmount
- destroyed → unmounted
6、/deep/ 和 ::v-deep
/deep/ 和 ::v-deep 替换为了 :deep() 方式
/* vue 2.x */
.modal {
/deep/ h4 {
text-align: left;
}
}
/* vue 3.0 */
.modal {
:deep(h4) {
text-align: left;
}
}
7、this.$ 实例全局方法
像this.$store、this.$http、this.$loading等方式需要正确声明后才能通过ts校验,
在shims-tsx.d.ts
里添加声明:
import { Store } from 'vuex'
import { State } from '@/store/index'
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$store: Store<State>;
$http: any;
$loading: {
show: (text?: string) => void;
hide: () => void;
setText: (text: string) => void;
};
}
}
8、vant按需导入
Vue.use已被移除,现在需要使用入口文件里的app实例通过app.use方式。
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { Toast, Lazyload } from 'vant'
const app = createApp(App)
app.use(Toast)
app.use(Lazyload)
app.use(store).use(router).mount('#app')
9、slot具名插槽
vue2.x里早期版本是通过name指定具名插槽的引用,在后来版本更新后支持通过template标签指定。
而vue3.0里做了统一,slot具名插槽引用时现在必须通过template标签来指定。
<!-- vue 2.x -->
<my-comp>
<i class="iconActive" slot="active-icon"></i>
<i class="iconInactive" slot="inactive-icon"></i>
</my-comp>
<!-- vue 3.0 -->
<my-comp>
<template #active-icon>
<i class="iconActive"></i>
</template>
<template #inactive-icon>
<i class="iconInactive"></i>
</template>
</my-comp>
10、$set
this.$set方法现在只在build构建环境时才有效,vue3.0使用proxy监听替代之前的Object.defineProperty,新的proxy api能监听到对象属性和数组项的增删,所以不再需要$set方法了。
// vue 2.x
this.$set(this.item, 'isGot', true)
// vue 3.0
this.item.isGot = true
11、onKeypress
由于web的KeyboardEvent.keyCode即将被废弃,所以vue不再支持按键码的修饰符事件,这会引起input标签的onKeypress方法和vue3.0绑定事件@input有冲突,导致堆栈溢出的报错。
建议将原生的onKeypress逻辑迁移到@keypress里。
12、ts校验:实例this上找不到属性
目前发现两种情况下会导致ts校验时报this上找不到属性,应该是vue的bug。
按以下两种方式检查处理你的代码:
- (1)找不到props里定义的变量。
解决方法:props类型校验部分,对象或数组类型定义默认值时换成箭头函数方式:
props: {
myObj: {
type: Object,
default: () => ({})
},
myArr: {
type: Array,
default: () => []
}
}
- (2)computed里找不到data里定义的变量。
解决方法:添加props属性。
export default defineComponent({
props: [],
data () {
return {
val: ''
}
},
computed: {
newVal (): string {
return 'got ' + val
}
}
})
13、.vue文件的ts声明
在文件shims-vue.d.ts
里,
// vue 2.x
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
// vue 3.0
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
14、嵌套路由
vue2.x里嵌套路由如果你的二级路由出口组件里定义的name属性值是’routerView’,没有问题。
而在vue3.0里就会出现死循环,需要换个name属性值,name值最好起的特殊些,不能和html原生或vue内置的标签名重名。
/**
* 路由出口
*/
<template>
<router-view/>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'out',
})
</script>
15、vue第三方组件
vue2.x的第三方组件在vue3.0里可能会有兼容性问题,如果是比较简单的组件,原作者没有跟进升级,你可以自己从组件github上把源码拉下来自己修改,修改方式和你自己的组件升级vue3.0方式一样;如果组件比较复杂,那就艾特原作者催更吧。
我项目只用到了vue-count-to,自己修改了下就能用了。
16、404页面
vue2.x的404页面只需要在路由末尾加个 * 匹配就行了。
而vue3.0换了一套全新的匹配方式,对于404页面,匹配方式改成如下:
{
path: '/:pathMatch(.*)*',
name: 'page404',
component: Page404
}
具体匹配到的路径可以在vue实例里通过this.$route.params.pathMatch
获取。
Tips:
pathMatch 是自定义的一个名字,随便取名,比如你定义的是abc,那就用this.$route.params.abc获取匹配路径。
17、vuex 和 vue-router
vuex 和 vue-router引入和定义的方式变了。
import { createStore } from 'vuex'
export default createStore({
modules: {},
state: {},
getters: {},
mutations: {},
actions: {}
})
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
const Index = () => import(/* webpackChunkName: "index" */ '@/views/index.vue')
const routes: Array<RouteRecordRaw> = [
{
path: '/index',
name: 'index',
component: Index,
}
]
const router = createRouter({
history: createWebHashHistory(), // history模式换成 history: createWebHistory()
routes: routes
})
export default router
18、mixins
vue3.0为了引入mixins时更方便维护数据,对data的合并规则做了一些修改,现在未在组件data里事先定义的变量不会合到组件里。
const Mixin = {
data() {
return {
user: {
name: 'Jack',
id: 1
}
}
}
}
const CompA = {
mixins: [Mixin],
data() {
return {
user: {
id: 2
}
}
}
}
合并结果:
// vue 2.x
{
user: {
id: 2,
name: 'Jack'
}
}
// vue 3.0
{
user: {
id: 2
}
}
(如果要在composition api方式里使用mixins,参考此文)
19、$on,$off 和 $once
$on,$off 和 $once 在vue3.0中已被移除,这些方法会使事件的注册和调用变的混乱,难以维护。
对于$on 和 $off,建议换成父子通讯的$emit来处理,$once之前项目里我只用于hook:beforeDestroy的处理,在vue3.0里只能把hook拎出来单独处理了。
20、过滤器filter
vue3.0已经移除了过滤器,其实在2.x里过滤器本身就是没必要的存在,因为所有过滤器都能被methods里定义一个方法来取代,而且过滤器只能用在dom结构里,定义的方法反而使用场景更广,那既然移除了,就都换成方法吧,只需接收参数返回一个新值即可。
全局filter可以通过app.config.globalProperties
注册方法实现。
21、transition过渡的class名
class-enter
修改为 class-enter-from
;
class-leave
修改为 class-leave-from
。
/* vue 2.x */
.fadeScale-enter,
.fadeScale-leave-to {
opacity: 0;
transform: scale(1.05);
}
/* vue 3.0 */
.fadeScale-enter-from,
.fadeScale-leave-to {
opacity: 0;
transform: scale(1.05);
}
22、$listeners
vue2.x里使用$attrs和$listeners对于组件的二次封装非常有用,
vue3.0里移除了$listeners,所有接收的事件都以on开头的属性存在$attrs里了,所以在vue3.0里只需要通过v-bind="$attrs"
即可把父组件传递的属性和方法全都绑定了,一步到位。
23、.native修饰符
vue3.0已经移除了事件的.native修饰符,3.0已经做了处理,默认情况会把绑定的原生事件名都当做组件根元素的原生事件处理,所以可以放心大胆的删掉.native。
四、其他
eslint和ts的配置可以参考:https://blog.csdn.net/u010059669/article/details/108323600