FionaRush / Article

Articles about technology
3 stars 0 forks source link

vue2 升级 vue3 方案 #7

Open FionaRush opened 3 years ago

FionaRush commented 3 years ago

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

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

安装使用

npm init vite-app hello-vue3 
# OR yarn create vite-app hello-vue3
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 改动如下:

升级方案: 找到使用的全局 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

2. `this.$slots` 将 slots 作为函数公开

> 升级方案:
> 1. 找到 `this.$scopedSlots` 并替换为 `this.$slots`;
> 2. 替换所有出现的 `this.$slots.mySlot` 与 `this.$slots.mySlot()`

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

```javascript
// 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. 生命周期

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

jangdelong commented 3 years ago

有在vue2项目上直接升级vue3吗?

FionaRush commented 3 years ago

有在vue2项目上直接升级vue3吗?

还没有实践,因为之前使用的 Element UI 还没有出 Vue3 的支持版本,现在已经支持了Vue3 Element UI ,有需要可以看一下

jangdelong commented 3 years ago

有在vue2项目上直接升级vue3吗?

还没有实践,因为之前使用的 Element UI 还没有出 Vue3 的支持版本,现在已经支持了Vue3 Element UI ,有需要可以看一下

我们现在在升级项目,升到vue3

FionaRush commented 3 years ago

有在vue2项目上直接升级vue3吗?

还没有实践,因为之前使用的 Element UI 还没有出 Vue3 的支持版本,现在已经支持了Vue3 Element UI ,有需要可以看一下

我们现在在升级项目,升到vue3

那你可以参考这个文档升级,这个得到大佬肯定的升级技术方案,只欠个落地

shuerguo999 commented 3 years ago

有在vue2项目上直接升级vue3吗?

有的,https://gogocode.io/zh/docs/vue/vue2-to-vue3