vue3升级方案(详解)

   随着 vue3 的发布和越来越多项目的使用,之前使用 vue2 的项目也不能拉下,vue2 升级 vue3 迫在眉睫。在升级之前你需要知道升级的内容和详细变更项。

   本文主要整理 vue2 升级 vue3 时,不兼变更的详解释和对比。这里有一份正式升级实践所需的 升级 todo 可供参考。

安装使用

  • 通过 CDN:<script src="https://unpkg.com/vue@next"></script>
  • 通过 Codepen 浏览器 playground
  • 脚手架 Vite:
npm init vite-app hello-vue3 
# OR yarn create vite-app hello-vue3
  • 脚手架 vue-cli:
npm install -g @vue/cli 
# OR yarn global add @vue/cli
vue create hello-vue3

Breaking

全局 API

1. 实例创建方式

// vue2
new Vue()  

// vue3
createApp()
2.x Global API3.x Instance API (app)
Vue.configapp.config
Vue.config.productionTipremoved ( see link )
Vue.config.ignoredElementsapp.config.isCustomElement ( see link )
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use ( see link)

2. 全局 API 调用方式改变

在 Vue3 中全局和内部 API 被重新构造为 tree-shakable 结构,以 ES 模块构建的命名导出方式访问全局 API。打包的时候不会把所有的 API 都打包进来,只会打包 import 的 API。涉及到的全局 API 改动如下:

  • Vue.nextTick
  • Vue.observable(替换为Vue.reactive)
  • Vue.version
  • Vue.h
  • Vue.compile(仅完整版本)
  • Vue.set(仅在兼容版本中)
  • Vue.delete(仅在兼容版本中)

升级方案: 找到使用的全局 API 后替换为 ES 模块

例如:

// vue2
import Vue from 'vue'
Vue.nextTick(() => { ... })

// vue3
import { nextTick } from 'vue'
nextTick(() => { ... })

data

  1. data 选项声明不再接受纯 object,需要通过函数返回对象的形式。将所有的 data 只接受 function return object ;
// vue2
<script>
<!--  Object 形式 data -->
  const app = new Vue({
    data: {
      apiKey: '123'
    }
  })
  
<!-- Function 形式 data -->
  const child = new Vue({
    data() {
      return {
        apiKey: 'a1b2c3'
      }
    }
  })
</script>

// vue3
<script>
  import { createApp } from 'vue'

  createApp({
    data() {
      return {
        apiKey: '123'
      }
    }
  }).mount('#app')
</script>
  1. 合并 data,使用 mixin 或扩展多个返回值时为浅合并,仅合并根级属性。

升级方案:

  1. 将共享数据提取到外部对象,并将其用作 data;
  2. 重写对共享数据的引用以指向新的共享对象;
  3. 将使用到的 minx 的 data 按照浅合并修改,必要时可以重构;

v-model

在 Vue2 中,v-model 指令必须使用为 value 的 prop,并且只允许在组件上使用一个 model 模块;
在 Vue 3 中,双向数据绑定的 API 更标准化,减少了使用 v-model 指令时的混淆并且更加灵活。
可以在同一个组件上使用多个 v-model 进行双向绑定,可以使用自定义 v-model 修饰符。

升级方案:

  1. 保留原来的 v-model 方式;
  2. 检查代码库的 .sync 使用情况,去掉了 .sync 修饰符,并将其替换为 v-model;
// vue2
<ChildComponent v-bind:name.sync="name" />

// vue3
<ChildComponent v-model:name="name"/>
  1. 对于没有参数的 v-model,确保将数据和事件分别更改为 modelValue 和 update:modelValue
// vue2
<ChildComponent v-model="pageTitle" />

// vue3
<ChildComponent
  :modelValue="pageTitle"
  @update:modelValue="pageTitle = $event"
/>

插槽统一

  1. this.$scopedSlots 改为 this.$slots
    在 Vue2 的某些场景,当有用到自定义 render 方法和插槽时,会用到this.$scopedSlots 获取数据。在 Vue3 中需要统一替代成 this.$slots。例如:
// vue2
h(LayoutComponent, [
  h('div', { slot: 'header' }, this.header),
  h('div', { slot: 'content' }, this.content)
])
this.$scopedSlots.header

// vue3
h(LayoutComponent, {}, {
  header: () => h('div', this.header),
  content: () => h('div', this.content)
})
this.$slots.header
  1. this.$slots 将 slots 作为函数公开

升级方案:

  1. 找到 this.$scopedSlots 并替换为 this.$slots
  2. 替换所有出现的 this.$slots.mySlotthis.$slots.mySlot()

watch

在 Vue2 中,使用 watch 监听时,如果监听对象层级较深,可以采用 “点分割” 的写法,例如:

// vue2
var vm = new Vue({
  data: {
    a: {
      b: {
      	c: 1
      }
    }
  },
  watch: {
    // watch vm.a.b's value 即 1
    'a.b': function (val, oldVal) { /* ... */ }
  }
})

在 Vue3 中不再支持点分隔字符串路径。为了发现对象内部值的变化,可以在选项参数中指定 deep: true
如果是 watch 数组,回调仅在替换数组时触发,不需要指定 deep: true 。

// vue3
const app = Vue.createApp({
 data() {
  return {
    a: {
      b: {
      	c: 1
      }
    }
  }
 },
 watch(
   () => this.a.b,
   (newVal, oldVal) => { // todo },
   { deep: true }
 )
})

升级方案:找到 watch 的 点分隔 字符串路径,改用 computed 作为参数

指令钩子

vue3 中指令的钩子函数仿照了组件中的钩子函数命名规则

// Vue 2
Vue.directive('highlight', {
  bind(el, binding, vnode) {
    el.style.background = binding.value
  },
  inserted(),
  beforeUpdate(),
  update(),
  componentUpdated(),
  beforeUnmount(),
  unbind()
})

// Vue 3
App.directive('highlight', {
  beforeMount(el, binding, vnode) { // 对应bind
    el.style.background = binding.value
  },
  mounted() {}, // 对应inserted
  beforeUpdate() {}, // 新增
  updated() {}, // 对应update
  beforeUnmount() {}, // 新增
  unmounted() {} // 对应unbind
})

升级方案:找到 所有的 directive 自定义指令,更新指令的生命周期,一般在项目的 src/directive 文件夹中

优先级

vue3 中 v-if 优先级始终高于 v-for

升级方案:

  1. 由于语法不明确,建议避免在同一个元素上同时使用这两个元素;
  2. 可以实现一种方法,创建一个计算属性,过滤出可见元素的列表。

v-bind 合并

vue3 中 v-bind 的绑定顺序将影响渲染结果

升级方案:
如果依赖于 v-bind 覆盖功能,确保 v-bind 先定义,再定义各个属性

key

  1. v-if / v-else / v-else-if 分支上不需要 key;
  2. <template v-for... > key 放置在 <template> 标签上;
// Vue 2
<template v-for="item in list">
  <div :key="item.id">...</div></template>

// Vue 3
<template v-for="item in list" :key="item.id">
  <div>...</div>
</template>

函数组件

在 Vue 2 中,函数式组件有两个主要作用:

  • 优化性能,函数组件 的初始化速度比 有状态组件 快
  • 返回多个根节点;

在 Vue 3 中,有状态组件的性能已经提高到可以忽略不计的程度。此外,有状态组件现在还包括返回多个根节点。因此,函数式组件剩下的唯一用例就是简单组件,比如创建动态标题的组件。

升级方案:

  1. 不再需要 functional:true 选项;
// Vue 2
const FunctionalComp = {
	functional: true,
	render(h) {
	  return h('div', `Hello! ${props.name}`)
	}
}
	
// Vue 3
import { h } from 'vue'
const FunctionalComp = (props, { slots, attrs, emit }) => {
	 return h('div', `Hello! ${props.name}`)
}
  1. functional 单文件组件(SFC)上移除 functional 属性 <template functional>
  2. 异步组件需 defineAsyncComponent 方法创建;
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => import('./Foo.vue'))

template

template 没有特殊指令( v-if / else-if / else,v-for 或 v-slot )的标记,视为普通元素,并将生成原生的 <template> 元素,而不是渲染其内部内容

升级方案:移除没有特殊指令的 <template>

过渡类名 Class

  1. <transition> 作为组件的根元素时,外部切换不会触发过渡效果。只能在 <transition> 内使用切换;
  2. 在 v-enter 过渡类已重命名为 v-enter-from;
  3. v-leave 过渡类已更名为 v-leave-from。

升级方案:

  1. 将 .v-enter 替换为 .v-enter-from;
  2. 将 .v-leave 替换为 .v-leave-from;
  3. 过渡组件相关属性名也需要进行字符串实例替换;

Render API

如果项目中使用<template>,则可忽略该条。

1、 h 由全局导入,不再作为参数传递给 render;例如:

// Vue 2
export default {
  render(h) {
    return h('div')
  }
}

// Vue 3
import { h } from 'vue'
export default {
  render() {
    return h('div')
  }
}

2、render 函数参数在有状态组件和功能组件之间变得更加一致;
3、整个 VNode props 的结构被展平。例如:

// vue2
{
  staticClass: 'button',
  class: {'is-outlined': isOutlined },
  staticStyle: { color: '#34495E' },
  style: { backgroundColor: buttonColor },
  attrs: { id: 'submit' },
  domProps: { innerHTML: '' },
  on: { click: submitForm },
  key: 'submit-button'
}

// vue3
{
  class: ['button', { 'is-outlined': isOutlined }],
  style: [
  	{ color: '#34495E' }, 
  	{ backgroundColor: buttonColor }
  ],
  id: 'submit',
  innerHTML: '',
  onClick: submitForm,
  key: 'submit-button'
}

升级方案:

  1. Vue 不应绑定到库中
  2. 对于模块构建,导入应该保持独立,由最终用户绑定器处理
  3. 对于 UMD/browser 版本,它应该首先尝试全局 Vue.h,然后回退以请求调用

prop

排查全部 prop 的 default,确保都不使用 this,3.x版本中 prop 的 default 不能访问 this ,可以使用 inject 来访问注入的 property 。

inheritAttrs

inheritAttrs: false 的组件,如果之前依赖 class 和 style,那么样式可能错误,因为这些属性可能应用待别的元素上了。
2.x 中 $attrs 包含了所有的 attribute 单没有 class 和 style;
3.x 中 $attrs 包含了所有的 attribute 包括 class 和 style,这些 attribute 都会应用到子组件上。

升级方案:
查找所有的设置了 inheritAttrs: false 的组件,确保升级后的样式是否正确。

自定义元素互操作

  1. 自定义元素白名单在模板编译期间执行,运行时配置进行配置改为通过编译器配置;
// vue2
Vue.config.ignoredElements = ['plastic-button']

// vue3
const app = Vue.createApp({})
app.config.isCustomElement = (
	tag: string
) => tag === 'plastic-button'
  1. 将所有组件标记的 is 用法更改为 v-is
// vue2
<tr is="blog-post-row"></tr>

// vue3
<tr v-is="'blog-post-row'"></tr>

Removed

1. v-on:keyCode

在 Vue2 中,我们可以通过 v-on:keyup.112 来指定按钮的触发事件,但是在 Vue3 中需要使用别名,例如 v-on:keyup.delete;

2. $on, $off, $once

在 Vue2.x 中可以通过 EventBus 的方法来实现组件通信。

var EventBus = new Vue()
Vue.prototype.$EventBus = EventBus
...
this.$EventBus.$on()  this.$EventBus.$emit()

Vue3 中移除了 $on, $once, $off 等方法,可以通过使用实现事件发射器接口的外部库来替换现有的事件中心。推荐使用 mitt 方案来代替。

import mitt from 'mitt'
const emitter = mitt()
// listen to an event
emitter.on('foo', e => console.log('foo', e) )
// fire an event
emitter.emit('foo', { a: 'b' })

3. filters

在 Vue3 中,移除了组件的 filters 项,可以使用 methods 的或者 computed 替代 。

4. inline-template

在 Vue3 中,移除了组件的 inline-template 项。
在 Vue2 中,在父组件引入子组件时,会用到 inline-template 来使子组件的内容也得到展示。例如:

<my-component inline-template>
  <div>
    <p>These are compiled as the component's own template.</p>
    <p>Not parent's transclusion content.</p>
  </div>
</my-component>

升级方案:

  1. 使用<script>标签;
  2. 默认插槽。

5. 生命周期

vue2vue3vue3 setup
beforeCreatebeforeCreatesetup() 内部
createdcreatedsetup() 内部
beforeMountbeforeMountonBeforeMount
mountedmountedonMounted
beforeUpdatebeforeUpdateonBeforeUpdate
updatedupdatedonUpdated
beforeDestroybeforeUnmountonBeforeUnmount
destroyedunmountedonUnmounted
errorCapturederrorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered

6. 实例 property

vm.$children
vm.$scopedSlots
vm.$isServer
vm.$listeners 合并至 $attrs


版权声明:本文为majing0520原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。