# Manual Migration Guide
Before manual migration, please make sure your project has been auto upgraded by vue-codemod (opens new window). Please refer to the User Guide (opens new window) 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 (opens new window) or PR (opens new window).
# Limitation
# Vue version
Vue version >= 2.6.0
# Third-party dependencies
Some third-party packages currently don't have support for Vue3
Currently, the UI framework libraries that support Vue3 are:
Please refer to Vue2ToVue3 (opens new window) to see the latest list of all the Vue3-supported UI Components and libraries.
# Vue
# Global API
Migration Guide from Vue.js team (opens new window)
Transform
Global API
to a pluginFor those global api not in
main.js
, transform them to a plugin form.In Vue2:
// directive/index.js import Vue from 'vue' import myDirective from '@/directive/myDirective' Vue.directive('myDirective', myDirective)
In Vue3:
// 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 Vue2:
// message/index.js Vue.prototype.$baseMessage = () => { Message({ offset: 60, showClose: true }) }
In Vue3:
// 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
.
# Slot
Please refer to Migration Guide from Vue.js team (opens new window) for more details.
slot
attributes are deprecated since Vue2.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>
# Filter
# Partial Filter
Please refer to Migration Guide from Vue.js team (opens new window) for more details.
# Global Filter
Please refer to Migration Guide from Vue.js team (opens new window) for more details.
# Events API
In Vue3, $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 (opens new window) or tiny-emitter (opens new window).
Please refer to Migration Guide from Vue.js team (opens new window) 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
# /deep/
>>>
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) {}
# Delimiter
In Vue2, event internal statement can use newline character
as the delimiter.
<button
@click="
item.value = ''
clearTag()
"
></button>
But in Vue3, newline character
is no longer used as the delimiter. A ;
or ,
is needed.
<button
@click="
item.value = '';
clearTag()
"
></button>
# Vue Router
# VueRouter.prototype
Please refer to Migration Guide from Vue.js team (opens new window) 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 Vue2) :
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 Vue3):
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)
}
})
}
# Removed *
routes
Please refer to Migration Guide from Vue.js team (opens new window) for more details.
Catch all routes (*
, /*
) must now be defined using a parameter with a custom regex.
In Router 3 (for Vue2), 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: '/'
}
]
# All navigations in Router 4 are asynchronous
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)
})
}
}
}
}
# Element-ui
Currently, Element UI (opens new window) provides a Vue3-supported libraries Element Plus (opens new window). 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.
# Import CSS
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')
# slot
attribute in el-table
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>