# 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 plugin

    • For 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 by window.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 in main.js can use window.app. The part of the code that is known to run after main.js are: 1. run inside the export default {}; 2. js files that use app.use() in main.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 dependencies

    yarn add mitt
    // or
    npm install mitt
    
  • Create mitt instance

    import 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>