Before manual migration, please make sure your project has been auto upgraded by vue-codemod. Please refer to the User Guide for using vue-codemod.
This manual migration guide is based on the actual problems encountered in the transformed project. Users could also encounter other problems in transforming their projects. It's welcomed for users to open an issue or PR.
Vue version >= 2.6.0
Some third-party packages currently don't have support for Vue 3
Currently, the UI framework libraries that support Vue 3 are:
Please refer to Vue2ToVue3 to see the latest list of all the Vue3-supported UI Components and libraries.
Migration Guide from Vue.js team
-
Transform
Global APIto a plugin-
For those global api not in
main.js, transform them to a plugin form.In Vue 2:
// directive/index.js import Vue from 'vue' import myDirective from '@/directive/myDirective' Vue.directive('myDirective', myDirective)
In Vue 3:
// directive/index.js import myDirective from '@/directive/myDirective' export default { install: app => { app.directive('myDirective', myDirective) } }
-
Import this plugin in
main.js// main.js import MyDirective from '@/directive' Vue.createApp(App).use(myDirective)
-
-
Transform
Global Configurationbywindow.app=app-
Configure the global app instance in
main.js// main.js const app = Vue.createApp(App) window.app = app // Configure the global app instance app.mount('#app')
-
Configuration not in
main.jsIn Vue 2:
// message/index.js Vue.prototype.$baseMessage = () => { Message({ offset: 60, showClose: true }) }
In Vue 3:
// message/index.js app.config.globalProperties.$baseMessage = () => { Message({ offset: 60, showClose: true }) }
⚠Attention: Users need to consider the execution order of the js code. Only the code that runs after the
window.app = appconfiguration statement inmain.jscan usewindow.app. The part of the code that is known to run after main.js are: 1. run inside theexport default {}; 2. js files that useapp.use()inmain.js.
-
Please refer to Migration Guide from Vue.js team for more details.
slot attributes are deprecated since Vue 2.6.0. v-slot was introduced for named and scoped slots. In vue-codemod , the slot-attribute rule can transform slot attributes to v-slot syntax:
<base-layout>
<p slot="content">2.5 slot attribute in slot</p>
</base-layout>will be transformed to:
<base-layout>
<template v-slot:content>
<p >2.5 slot attribute in slot</p>
</template>
</base-layout>For those named slots that use v-if and v-else together, vue-codemod will return an error.
<el-button v-if="showCronBox" slot="append" @click="showBox = false"></el-button>
<el-button v-else="showCronBox" slot="append" @click="showBox = true"></el-button>will be transformed to:
<template v-slot:append>
<el-button v-if="showCronBox" @click="showBox = false"></el-button>
</template>
<template v-slot:append>
<el-button v-else="showCronBox" slot="append" @click="showBox = true"></el-button>
</template>Since v-if and v-else will be divided into two <template>, it will return an error:
v-else used on element <el-button> without corresponding v-if.We need to manually put v-if and v-else into one <template> tag.
<template v-slot:append>
<el-button v-if="showCronBox" @click="showBox = false"></el-button>
<el-button v-else="showCronBox" slot="append" @click="showBox = true"></el-button>
</template>Please refer to Migration Guide from Vue.js team for more details.
Please refer to Migration Guide from Vue.js team for more details.
In Vue 3, $on, $off and $once instance methods are removed. Component instances no longer implement the event emitter interface, thus it is no longer possible to use these APIs to listen to a component's own emitted events from within a component. The event bus pattern can be replaced by using an external library implementing the event emitter interface, for example mitt or tiny-emitter.
Please refer to Migration Guide from Vue.js team for more details.
-
Add
mittdependenciesyarn add mitt // or npm install mitt
-
Create
mittinstanceimport mitt from 'mitt' const bus = {} const emitter = mitt() bus.$on = emitter.on bus.$off = emitter.off bus.$once = emitter.once export default bus
-
Add global event bus declaration in
main.js// main.js import bus from '@/bus' const app = createApp(App).mount('#app') app.config.globalProperties.$bus = bus
>>>and/deep/are not supported/deep/ .el-input {}should be transformed to:deep(.el-input) {}v-deep:: .bar {}should be transformed to::v-deep(.bar) {}
In Vue 2, event internal statement can use newline character as the delimiter.
<button @click="
item.value = ''
clearTag()
">
</button>But in Vue 3, newline character is no longer used as the delimiter. A ; or , is needed.
<button @click="
item.value = '';
clearTag()
">
</button>Please refer to Migration Guide from Vue.js team for more details.
In Router 3, Vue Router is a class, which can use prototype to access push method. But in Router 4, Router is an instance, which needs to access the push method through an instance.
In Router 3 (for Vue 2) :
import VueRouter from 'vue-router'
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function (location, onResolve, onReject) {
if (onResolve || onReject) {
return originalPush.call(this, location, onResolve, onReject)
}
return original.call(this, location).catch(e => {
if (e.name !== 'NavigationDuplicated') {
return Promise.reject(e)
}
})
}In Router 4 (for Vue 3):
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
// Attention: Rewrite the push method after creating the router instance.
const originalPush = router.push
router.push = function (location, onResolve, onReject) {
if (onResolve || onReject) {
return originalPush.call(this, location, onResolve, onReject)
}
return original.call(this, location).catch(e => {
if (e.name !== 'NavigationDuplicated') {
return Promise.reject(e)
}
})
}Please refer to Migration Guide from Vue.js team for more details.
Catch all routes (*, /*) must now be defined using a parameter with a custom regex.
In Router 3 (for Vue 2), users can define * router directly:
// router/index.js
const asyncRoutes = [
{
path: '*',
redirect: '/'
}
]In Router 4 (for Vue 4), users need to use pathMatch to define path:
// router/index.js
const asyncRoutes = [
{
path: '/:pathMatch(.*)*',
redirect: '/'
}
]It may caused some render failure for components. For example, the following RuleFilter.vue component:
watch: {
$route: {
immediate: true,
handler (to, from) {
if (to.name === 'RuleFilterTbl') {
const param = !!this.$refs.internal ? this.$refs.internal.selectItem : {}
this.$bus.$emit('filterSearch', param)
}
}
}
}this.$bus.$emit will return an error, because the event has not been created. The event will not be registered on $bus until the component is mounted.
// RuleFilterTbl.vue
mounted() {
this.$bus.$on('filterSearch', this.search)
this.$bus.$on('filterReset', this.reset)
}So you may need to wait for the router to be ready before trigger filterSearch:
watch: {
$route: {
immediate: true,
handler (to, from) {
if (to.name === 'RuleFilterTbl') {
const param = !!this.$refs.internal ? this.$refs.internal.selectItem : {}
// Determine whether the router is initialized
this.$router.isReady().then(() => {
this.$bus.$emit('filterSearch', param)
})
}
}
}
}Currently, Element UI provides a Vue3-supported libraries Element Plus. vue-codemod has completed most of the upgrade scenarios such as dependency upgrade and dependency replacement, but Element-Plus is still in beta testing, some functions may be unstable, and developers need to upgrade manually.
Part of global CSS should be imported from element-plus: import('element-ui/lib/theme-chalk/index.css') should be replaced with import('element-plus/lib/theme-chalk/index.css')
Must use <template> to wrap the slot. For example:
<el-table>
<span slot-scope='scope'>{{ scope.row.num }}</span>
</el-table>Need to be transformed to:
<el-table>
<template #default='scope'>
<span>{{ scope.row.num }}</span>
</template>
</el-table>