# 手动迁移指导

手动迁移之前,请先使用 vue-codemod (opens new window) 工具进行自动转换。工具使用方法请参考 使用指导 (opens new window)

本迁移手册是基于已经转换的项目中的实际遇到的问题进行总结的,在更多真实项目的转换过程中,可能会遇到其他问题,欢迎提交 issue (opens new window) 或者 PR (opens new window) 进行贡献。

# 限制

# Vue 版本

Vue 的版本需要不低于 2.6.0,如果使用了 2.6.0 之前的版本,可能部分语法无法自动转换。

# 第三方依赖

dependency 中有不支持 Vue3 的库,暂时无法实现升级,需要选择其他依赖库替代,并重写部分逻辑。

目前已经支持 Vue3 的 UI 框架库有:

具体是否支持 Vue3 ,可参考 Vue2ToVue3 (opens new window)

# Vue

# 全局 API

官方迁移指导 (opens new window)

  • 使用插件的方式对全局 API 进行转换

    • 将非 main.js 中的全局 API 装载为插件形式

      Vue2 中:

      // directive/index.js
      import Vue from 'vue'
      import myDirective from '@/directive/myDirective'
      
      Vue.directive('myDirective', myDirective)
      

      Vue3 中:

      // directive/index.js
      import myDirective from '@/directive/myDirective'
      
      export default {
        install: app => {
          app.directive('myDirective', myDirective)
        }
      }
      
    • 并在 main.js 中引用使用该插件

      // main.js
      import MyDirective from '@/directive'
      
      Vue.createApp(App).use(myDirective)
      
  • 使用 window.app=app 的方式对全局配置进行转换

    • main.js 中配置全局 app 实例

      // main.js
      const app = Vue.createApp(App)
      window.app = app // 配置全局 app 实例
      app.mount('#app')
      
    • 非 main.js 中的配置

      Vue2 中:

      // message/index.js
      Vue.prototype.$baseMessage = () => {
        Message({
          offset: 60,
          showClose: true
        })
      }
      

      Vue3 中:

      // message/index.js
      app.config.globalProperties.$baseMessage = () => {
        Message({
          offset: 60,
          showClose: true
        })
      }
      

      注意:使用 window.app 需要结合 Vue 框架中 js 代码的执行顺序,只有在 main.js 中 window.app = app 配置语句之后运行的代码才可以使用 window.app 。已知在 main.js 之后运行的代码部分为:(1) 在 Vue 组件的 export default {} 中运行;(2) 在 main.js 使用 app.use() 的 js 文件。

# 插槽

详情见:官方迁移指导 (opens new window)

slot 属性的语法在 Vue2.6.0 版本开始被废弃了,需要使用 v-slot 指令来支持具名插槽。转换工具的 slot-attribute 规则会将 slot 属性转换为 v-slot 指令的用法:

<base-layout>
  <p slot="content">2.5 slot attribute in slot</p>
</base-layout>

会被转换为:

<base-layout>
  <template v-slot:content>
    <p>2.5 slot attribute in slot</p>
  </template>
</base-layout>

对于同时使用了 v-ifv-else 指定的具名插槽来说,工具的转换则会产生错误:

<el-button
  v-if="showCronBox"
  slot="append"
  @click="showBox = false"
></el-button>
<el-button
  v-else="showCronBox"
  slot="append"
  @click="showBox = true"
></el-button>

将会被转换为:

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

由于 v-ifv-else 被分隔到两个 <template> 中,将会编译报错找不到 v-if ,需要将 v-ifv-else 放到同一个 <template> 标签范围内:

<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

详情见官方迁移指导 (opens new window)

# 全局 filter

详情见官方迁移指导 (opens new window)

# 事件 API

在 Vue3 中,$on$off$once 实例方法已经被移除,应用实例不再实现事件触发接口,因此无法使用这些 API 从组件内部监听组件自己发出的事件。但是该 eventHub 模式可以被替换为实现了事件触发器接口的外部库。

详情见官方迁移指导 (opens new window)

  • 添加 mitt 依赖

    yarn add mitt
    // or
    npm install mitt
    
  • 创建 mitt 实例

    import mitt from 'mitt'
    
    const bus = {}
    const emitter = mitt()
    bus.$on = emitter.on
    bus.$off = emitter.off
    bus.$once = emitter.once
    
    export default bus
    
  • main.js 中添加全局事件总线声明

    // main.js
    import bus from '@/bus'
    
    const app = createApp(App).mount('#app')
    app.config.globalProperties.$bus = bus
    

# /deep/样式

  • >>>/deep/ 已经不支持了
  • /deep/ .el-input {} 更改为 :deep(.el-input) {}
  • v-deep:: .bar {} 更改为 ::v-deep(.bar) {}

# 分隔符

Vue2 中事件内部语句可以通过换行符作为分隔符:

<button
  @click="
  item.value = ''
  clearTag()
"
></button>

但是在 Vue3 中,需要添加 ; 或者 , 作为分隔符:

<button
  @click="
  item.value = '';
  clearTag()
"
></button>

# Vue Router

# VueRouter.prototype

详情见:官方迁移指导 (opens new window)

Router 3 中,Vue Router 是一个类,可以通过 prototype 访问 push 方法,但是在 Router 4 中,Router 是一个实例,需要通过实例去访问 push 方法。

在 Router 3(配套 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)
    }
  })
}

在 Router 4(配套 Vue 3)中:

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})
// 注意:重写 push 方法要在创建 router 实例之后
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)
    }
  })
}

# path 中的通配符 *

详情见:官方迁移指导 (opens new window)

现在必须使用指定的 regex 参数来定义所有路由(*/*)。

在 Router 3(配套 Vue 2)中,可以直接定义 * 路由:

// router/index.js
const asyncRoutes = [
  {
    path: '*',
    redirect: '/'
  }
]

在 Router 4(配套 Vue 3)中,则需要使用 pathMatch 来定义 path:

// router/index.js
const asyncRoutes = [
  {
    path: '/:pathMatch(.*)*',
    redirect: '/'
  }
]

# Router 4 所有的导航都是异步的

可能会导致部分组件渲染失败,例如下面的 RuleFilter.vue 组件:

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 将会激发事件失败,因为事件还不存在,事件在组件挂载之后才会注册到 $bus 上:

// RuleFilterTbl.vue
mounted() {
  this.$bus.$on('filterSearch', this.search)
  this.$bus.$on('filterReset', this.reset)
}

所以触发 filterSearch 事件时需要等待组件挂载,修改为:

watch: {
  $route: {
    immediate: true,
    handler (to, from) {
      if (to.name === 'RuleFilterTbl') {
        const param = !!this.$refs.internal ? this.$refs.internal.selectItem : {}
        // 判断路由是否完成初始化
        this.$router.isReady().then(() => {
          this.$bus.$emit('filterSearch', param)
        })
      }
    }
  }
}

# Element-ui

目前 Element UI (opens new window) 提供了适配 Vue3 的组件库 Element Plus (opens new window)vue-codemod 完成了依赖升级与依赖替换等大部分的升级场景,但是 Element-Plus 仍然处于 beta 测试中,部分功能可能不稳定,需要开发者手动升级。

# 样式引入

部分全局样式的引入需要手动替换路径:import('element-ui/lib/theme-chalk/index.css') 替换为 import('element-plus/lib/theme-chalk/index.css')

# 表格 el-table 作用域插槽

必须使用 <template> 配合 slot 的形式,例如:

<el-table>
  <span slot-scope="scope">{{ scope.row.num }}</span>
</el-table>

需要切换成:

<el-table>
  <template #default="scope">
    <span>{{ scope.row.num }}</span>
  </template>
</el-table>