随着 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 API | 3.x Instance API (app) |
---|---|
Vue.config | app.config |
Vue.config.productionTip | removed ( see link ) |
Vue.config.ignoredElements | app.config.isCustomElement ( see link ) |
Vue.component | app.component |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vue.use | app.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
- 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>
- 合并 data,使用 mixin 或扩展多个返回值时为浅合并,仅合并根级属性。
升级方案:
- 将共享数据提取到外部对象,并将其用作 data;
- 重写对共享数据的引用以指向新的共享对象;
- 将使用到的 minx 的 data 按照浅合并修改,必要时可以重构;
v-model
在 Vue2 中,v-model 指令必须使用为 value 的 prop,并且只允许在组件上使用一个 model 模块;
在 Vue 3 中,双向数据绑定的 API 更标准化,减少了使用 v-model 指令时的混淆并且更加灵活。
可以在同一个组件上使用多个 v-model 进行双向绑定,可以使用自定义 v-model 修饰符。
升级方案:
- 保留原来的 v-model 方式;
- 检查代码库的 .sync 使用情况,去掉了 .sync 修饰符,并将其替换为 v-model;
// vue2
<ChildComponent v-bind:name.sync="name" />
// vue3
<ChildComponent v-model:name="name"/>
- 对于没有参数的 v-model,确保将数据和事件分别更改为 modelValue 和 update:modelValue
// vue2
<ChildComponent v-model="pageTitle" />
// vue3
<ChildComponent
:modelValue="pageTitle"
@update:modelValue="pageTitle = $event"
/>
插槽统一
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
this.$slots
将 slots 作为函数公开
升级方案:
- 找到
this.$scopedSlots
并替换为this.$slots
;- 替换所有出现的
this.$slots.mySlot
与this.$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
升级方案:
- 由于语法不明确,建议避免在同一个元素上同时使用这两个元素;
- 可以实现一种方法,创建一个计算属性,过滤出可见元素的列表。
v-bind 合并
vue3 中 v-bind 的绑定顺序将影响渲染结果
升级方案:
如果依赖于 v-bind 覆盖功能,确保 v-bind 先定义,再定义各个属性
key
- v-if / v-else / v-else-if 分支上不需要 key;
<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 中,有状态组件的性能已经提高到可以忽略不计的程度。此外,有状态组件现在还包括返回多个根节点。因此,函数式组件剩下的唯一用例就是简单组件,比如创建动态标题的组件。
升级方案:
- 不再需要 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}`)
}
- functional 单文件组件(SFC)上移除 functional 属性
<template functional>
;- 异步组件需 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
- 当
<transition>
作为组件的根元素时,外部切换不会触发过渡效果。只能在<transition>
内使用切换; - 在 v-enter 过渡类已重命名为 v-enter-from;
- v-leave 过渡类已更名为 v-leave-from。
升级方案:
- 将 .v-enter 替换为 .v-enter-from;
- 将 .v-leave 替换为 .v-leave-from;
- 过渡组件相关属性名也需要进行字符串实例替换;
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'
}
升级方案:
- Vue 不应绑定到库中
- 对于模块构建,导入应该保持独立,由最终用户绑定器处理
- 对于 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 的组件,确保升级后的样式是否正确。
自定义元素互操作
- 自定义元素白名单在模板编译期间执行,运行时配置进行配置改为通过编译器配置;
// vue2
Vue.config.ignoredElements = ['plastic-button']
// vue3
const app = Vue.createApp({})
app.config.isCustomElement = (
tag: string
) => tag === 'plastic-button'
- 将所有组件标记的 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>
升级方案:
- 使用
<script>
标签;- 默认插槽。
5. 生命周期
vue2 | vue3 | vue3 setup |
---|---|---|
beforeCreate | beforeCreate | setup() 内部 |
created | created | setup() 内部 |
beforeMount | beforeMount | onBeforeMount |
mounted | mounted | onMounted |
beforeUpdate | beforeUpdate | onBeforeUpdate |
updated | updated | onUpdated |
beforeDestroy | beforeUnmount | onBeforeUnmount |
destroyed | unmounted | onUnmounted |
errorCaptured | errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked | |
renderTriggered | onRenderTriggered |
6. 实例 property
vm.$children
vm.$scopedSlots
vm.$isServer
合并至 vm.$listeners
$attrs