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 API
to 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 Configuration
bywindow.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.js
In 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 = app
configuration statement inmain.js
can 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
mitt
dependenciesyarn add mitt // or npm install mitt
-
Create
mitt
instanceimport 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>