项目框架由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里直接调用隐藏组件功能时会有警告,导致mountedbeforeUnmount钩子不执行,暂不清楚是不是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

Logo

前往低代码交流专区

更多推荐