likes
comments
collection
share

Vue2 迁移 Vue3 实践

作者站长头像
站长
· 阅读数 43

Vue2 升级到 Vue3 使用的是 GoGoCode 代码转换工具,通过工具自动化完成框架升级、代码重构、多平台转换等工作。然后在转换的基础上,使用 vue2-to-composition-api 把简单的功能点逐个改造为 Vue3 Composition API,最后修改升级后不兼容、无法转换的点。

非兼容性改变改造

Vue 官方链接

官网非兼容性改变详情GoGoCode 转换支持度
全局 API全局 Vue API 更改为使用使用程序实例支持
全局和内部 API 支持 TreeShaking支持
模板指令v-model,替换掉 v-bind.sync支持
key 使用改变支持
v-if 和 v-for 优先级改变支持
v-bind 合并顺序行为支持
v-on.event.native 移除支持
组件函数式组件支持
functional 移除支持
defineAsyncComponent 创建异步组件支持
组件事件使用 emits 选项进行声明支持
渲染函数渲染函数 API 更改支持
移除 scopeSlots,统一使用scopeSlots,统一使用 scopeSlots,统一使用slots支持
listeners移除并合并到listeners 移除并合并到 listeners移除并合并到attrs 中支持
$attrs 包含 class 和 style支持
自定义元素无需转换
其他小改变VNode 生命周期事件开发中(需要自己处理)1) destroyed 更名为 unmounted 2) beforeDestroy 更名为 beforeUnmount
Props 的默认函数访问 this无需转换
自定义指令支持
Data 选项应始终被声明为函数支持
Transition class 名更改支持
Watch 侦听数组支持
移除的 API按键修饰符支持
on、on、onoff、$once 实例方法移除支持
过滤器 filter支持
全局函数 set、delete 及实例方法 set、set、setdelete 不在需要手动更改相关代码
内联模块移除不支持 1)对于“布尔 attribute” falsy类型移除,非falsy类型加上 2)对于“枚举 attribute” 改为字符串
$children不支持 - 使用模板引用 ref 替换
propsData 选项不支持- 通过 createApp 的第二个参数 propsData

vue2-to-composition-api 转换规则

Props / Data 数据转换

Vue2 迁移 Vue3 实践

以上转换后使用注意:

  • props 使用时相当于把 Vue2 使用的 this. 替换为 props.
  • data 使用时相当于把 Vue2 使用的 this. 替换为 data.
  • props、data 在 js 中使用按上面两条使用即可,但是需要注意在  中使用需要加上 props.、data.。

Computed 计算器属性转换

Vue2 迁移 Vue3 实践

注意:

  • computed 计算属性,在 js 中使用时需要加上 .value

Watch 侦听转换

Vue2 迁移 Vue3 实践

生命周期转换

Vue2 迁移 Vue3 实践

Methods 方法转换

Vue2 迁移 Vue3 实践

无法解析的内容

不能转换 Mixin、Component 等外部内容,转换器无法解析外部的文件,Mixin 混入内部的变量与方法都需另外手工处理,动态变量或者拼接的内容也同样无法被解析或解析错误

export default {
  name: 'Sample',
  mixins: [myMixin],
  components: { Echart },
  methods: {
    onSubmit(propName) {
      this[propName] = '123'
      this.$emit(propName + '-change')
    }
  }
}

踩坑点

$parent

  • $parent 在 vue3 中会获取到 transition 节点

$children

此属性在 Vue3 上已移除,想达到相应效果需要使用如下方式: 方式一:使用 ref

<template>
  <my-component ref="myComponent"></my-component>
</template>
<script setup>
	import { ref } from 'vue';

  // 声明一个 ref 来存放该元素的引用
	// 必须和模板里的 ref 同名
  const myComponent = ref(null);
</script>

注意:

  • 使用 ref 模板引用获取子组件的方式,能获取到的内容为子组件暴露出的内容(使用 Composition API 的情况下)
  • defineExpose 编译器宏来显式指定在

方式二:使用 hooks

  • 定义 hook
import { getCurrentInstance } from "vue";

function $walk(vNode, children) {
  if (vNode.component && vNode.component.proxy) {
    children.push(vNode.component.proxy);
  } else if (vNode.shapeFlag & (1 << 4)) {
    const vNodes = vNode.children;
    for (let i = 0; i < vNodes.length; i++) {
      $walk(vNodes[i], children);
    }
  } else if (vNode.component && vNode.component.subTree) {
    $walk(vNode.component.subTree, children);
  }
}

export function useChildren() {
  const currentInstance = getCurrentInstance()
  
  function getChildren(instance) {
    const internalInstance = instance ?? currentInstance
    const root = internalInstance.subTree;
    const children = [];
    if (root) {
      $walk(root, children);
    }
    return children;
  }
  
  return getChildren
}
  • 使用
<template>
  <my-component></my-component>
</template>
<script setup>
	import { useChildren } from 'useChildren.js';
  
  const getChildren = useChildren();
  getChildren();
</script>

方式三:

export function findChildren(parent, matcher) {
  const found = [];
  const root = parent.$.subTree;
  walk(root, child => {
    if (!matcher || matcher.test(child.$options.name)) {
      found.push(child);
    }
  });
  return found;
}

function walk(vnode, cb) {
  if (!vnode) return;

  if (vnode.component) {
    const proxy = vnode.component.proxy;
    if (proxy) cb(vnode.component.proxy);
    walk(vnode.component.subTree, cb);
  } else if (vnode.shapeFlag & 16) {
    const vnodes = vnode.children;
    for (let i = 0; i < vnodes.length; i++) {
      walk(vnodes[i], cb);
    }
  }
}

调用示例: const found = findChildren(this, /^(OSelect|OInput|OInputitems)$/);

emitted

  • Component emitted event "on-group-click" but it is neither declared in the emits option nor as an "onOn-group-click" prop

问题:在点击事件代码中使用了 $emit,但是事件没有在 emits 中显式声明 解决:在要触发该事件的的组件中显式声明该事件。

v-model 与 :value

如果需要保持 v-model="value" 的写法,则需要在组件内部将 value 改成 modelValue 属性,也可以通过给 v-model 指定一个参数来更改这些名字:

<MyComponent v-model:title="bookTitle" />

在这个例子中,子组件应声明一个 title prop,并通过触发 update:title 事件更新父组件值:

<!-- MyComponent.vue -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>

<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

Mixins

可以使用 Composition API 的形式 替换掉 Mixins,比如:

  • 转换前:
//MixinComponent.js

export default{
  data: ()=>({
    reuseData: "Mixin data";
  }),
  methods: {
    reuseMethod() {
      console.log('Hello from mixin!')
    }       
  }
}
  • 转换后:
//useComp.js

import {ref} from 'vue'
export default function useComp(){

  const reuseData = ref ("Reusable data")
  function reuseMethod() {
    console.log(reuseData.value);
    console.log('Hello from Reusable method!')
  }
  return {
    reuseData,
    reuseMethod
  }
}
  • 使用:
<script setup>
 	import useComp from './useComp.js';
  const { reuseData, reuseMethod } = useComp();
	console.log('reuseData', reuseData);
  reuseMethod();
</script>

组件 name 问题

方式一:Options API

<template>
  <div>Hello {{ msg }}!</div>
</template>

<script>
import { ref } from 'vue';

export default {
  name: 'myComponent',
  setup: () => {
    const msg = ref('World')
    return {
      msg
    }
  }
}
</script>

方式二:双 script 标签

<template>
  <div>Hello {{ msg }}!</div>
</template>

<script setup>
import { ref } from 'vue';

const msg = ref('World');
</script>
<script>
  export default {
  	name: 'myComponent'
	}
</script>

方式三:使用插件

  • 使用插件 unplugin-vue-define-options

安装:

yarn add -D unplugin-vue-define-options

Vite 配置修改

// vite.config.ts
import DefineOptions from 'unplugin-vue-define-options/vite'

export default defineConfig({
  plugins: [DefineOptions()],
})

TypeScript 支持

// tsconfig.json
{
  "compilerOptions": {
    // ...
    "types": ["unplugin-vue-define-options/macros-global" /* ... */]
  }
}

使用

<script setup lang="ts">
  import { useSlots } from 'vue'
  defineOptions({
    name: 'MyComponent',
    inheritAttrs: false,
  })
  const slots = useSlots()
</script>
  • 安装插件 vite-plugin-vue-setup-extend,该插件的作用是 vue script setup 语法支持 name 属性。
<template>
  <div>Hello {{ msg }}!</div>
</template>

<script setup name="myComponent">
import { ref } from 'vue';

const msg = ref('World');
</script>

props 转换问题

<!-- 仅写上 prop 但不传值,会隐式转换为 `true` -->
<my-component canCancel="" />
<my-component canCancel />

$slots

Vue3 的 slots 统一使用 $slots 表示父组件所传入插槽的对象。

每一个插槽都在 this.slots 上暴露为一个函数,返回一个 vnode 数组,同时 key 名对应着插槽名。默认插槽暴露为 this.slots.default。

template

在 Vue2 中,单独使用  里面的元素会被展示出来 在 Vue3 中,单独使用,父元素或者父组件没有定义 default 插槽,那么  就会隐藏,导致内部元素不展示: Vue2 迁移 Vue3 实践

h 函数

// 完整参数签名
function h(
  type: string | Component,
  props?: object | null,
  children?: Children | Slot | Slots
): VNode

  // 省略 props
function h(type: string | Component, children?: Children | Slot): VNode

type Children = string | number | boolean | VNode | null | Children[]

type Slot = () => Children

type Slots = { [name: string]: Slot }

第一个参数为 string 时会直接把内容作为标签,而不会像 Vue2 时转换成组件,需要手动使用 resolveComponent(xxx) 解析组件或者使用 import 导入组再使用。

import MyComponent from './MyComponent.vue'

// 传递 prop
h(MyComponent, {
  // 等价于 some-prop="hello"
  someProp: 'hello',
  // 等价于 @update="() => {}"
  onUpdate: () => {}
})

// or

// 传递 prop
h(resolveComponent('my-component'), {
  // 等价于 some-prop="hello"
  someProp: 'hello',
  // 等价于 @update="() => {}"
  onUpdate: () => {}
})

如果 h 函数传递的第二个参数 props 使用 on 对象批量定义事件,需要进行转换才能使用,转换方法

/**
 * 短横线命名 转 帕斯卡命名
 * @param value
 * @returns {string}
 */
const kebabToPascal = (value) => {
  let words = value.split('-');
  words = words.reduce((prev, word) => {
    return prev + word.slice(0, 1).toUpperCase() + word.slice(1);
  });
  words = words.slice(0, 1).toUpperCase() + words.slice(1);
  return words;
};

/**
 * 判断是否是对象
 * @param target
 * @returns {boolean}
 */
const isObject = (target) => Object.prototype.toString.call(target) === '[object Object]';

/**
 * 转换参数中 on 定义的事件为 onXxx
 * @param payload
 * @returns {*}
 */
export function plantOnPara(payload) {
  if (!isObject(payload) || !isObject(payload.on)) {
    return payload
  }
  let result = {}
  const events = payload.on
  for (const key in events) {
      if (Object.hasOwnProperty.call(events, key)) {
        result[`on${kebabToPascal(key)}`] = events[key]
      }
  }
  delete payload.on
  return {
    ...payload,
    ...result
  }
}

因为 h 函数的第二个参数 props 是扁平化的,所以 props 的值需要进行转换

// 方法来自 GoGoCode
export function plantRenderPara(params) {
  const transProps = {
    staticClass: 'class',
    staticStyle: 'style',
    on: '',
    domProps: '',
    props: '',
    attrs: '',
  }
  function obj2arr(obj) {
    return typeof obj == 'string'
      ? [obj]
      : Object.keys(obj).map((pk, index) => {
          return { [pk]: Object.values(obj)[index] }
        })
  }
  let result = {}
  for (let key in params) {
    if (transProps[key] == null) {
      if (typeof params[key] == 'object') {
        result[key] = obj2arr(params[key])
      } else {
        result[key] = params[key]
      }
    }
  }
  for (let key in params) {
    if (transProps[key] === '') {
      if (typeof params[key] == 'object') {
        for (let k in params[key]) {
          result[k] = params[key][k]
        }
      } else {
        result[key] = params[key]
      }
    }
  }
  for (let key in params) {
    if (transProps[key]) {
      result[transProps[key]] = result[transProps[key]] || []
      result[transProps[key]] = result[transProps[key]].concat(
        obj2arr(params[key])
      )
    }
  }
  return result
}

参考