vue2组件库升级vue3实践

1,096 阅读7分钟

long long ago 做了一个Vue低码工具相关的组件库,由于新业务系统需要,想把这个应用于新业务系统中,满心欢喜的开始接入ing, 突然发现....新的业务系统是Vue3的......

image.png

旧的组件库是采用vue2并基于element-ui封装的,新的业务系统除了用的是vue3 ,组件库也是使用了element-plus,强行集成一波后......

image.png

自此开始了组件库升级Vue3版本的改造之路......

开始改造

在此记录我的改造经验,也希望可以为需要的伙伴们提供些参考

关于组合式API

各组件中的写法均改为了组合式API,关于组合式API的介绍和优点可以参考官网给出的什么是组合式API ?,改为组合式API后随之的生命周期的写法,data和props以及watch等的写法都发生了一些变化,这里不做过多赘述,这块信官网得永生~~总体体验下来,这种写法还是蛮舒服的,更贴近函数式编程,逻辑抽离组织起来也很方便。

另外需要注意的是关于生命周期,除了引入方式变更外:destroyed 生命周期选项被重命名为 unmounted;beforeDestroy 生命周期选项被重命名为 beforeUnmount。

关于 ref()

之前在vue2 中我们定义组件的数据一般在 data 中去定义,而在 vue3 的setup 函数中,我们可以使用 ref API 来做响应式数据,ref() 返回的是一个 value reference (包装对象) 。一个包装对象只有一个属性: .value ,该属性指向内部被包装的值。这块使用起来并不难,主要是理解为什么在vue3中使用包装对象呢?

我们知道在js中原始类型是只有值的,没有引用,如果一个函数返回一个字符串,那我们接收这个字符串时也只是接收到了一个值而已,它后续的变化我们无法去追踪,基于这个原因我们膜拜下尤雨溪大佬对这块的设计: 包装对象的意义就在于提供一个让我们能够在函数之间以引用的方式传递任意类型值的容器。这有点像 React Hooks 中的 useRef —— 但不同的是 Vue 的包装对象同时还是响应式的数据源。有了这样的容器,我们就可以在封装了逻辑的组合函数中将状态以引用的方式传回给组件。组件负责展示(追踪依赖),组合函数负责管理状态(触发更新)

这个可以帮助我们更好的理解为何在vue3中我们使用类似ref这样的API ,不过如果想创建一个没有包装的响应式对象,我们可以使用 reactive API(和 2.x 的 Vue.observable() 等同)。

另外当包装对象被暴露给模版渲染上下文,或是被嵌套在另一个响应式对象中的时候,它会被自动展开 (unwrap) 为内部的值,所以我们在使用的时候并不需要通过 .value 去取值。

关于渲染函数

在vue的组件库中,我们免不了会用函数式的方法去写一些组件,也就是Render 函数,在Vue2中我们这么用:

render(h) {
  return h('h1', {
    'class': 'm-h'
  }, '标题')
}

但是在 vue3 中这样是不行的,需要改写为:

import { h } from 'vue';
render() {
  return h('h1', {
    'class': 'm-h'
  }, '标题')
}

在 vue3 中不再从render函数中隐式的提供 h ,而是全局引入 { h } from ‘vue’。 h 函数用于创建vnode,h 函数接收三个参数: 一个 HTML 标签名、一个组件、一个异步组件或函数式组件。

思考:为什么不再隐式的提供 h 函数?我个人的理解是,这样的话我们可以根据我们的需要,从setup中直接返回一个渲染函数而不是再必须从render中返回,我们可以使用如下的方式:

import { h } from 'vue'
const myComponent = () => {
  setup() {
    .....
    return (props) => {
      h('div', {}, 'demo')
    }
  }
}

关于mixin

之前写一个vue2的组件库,肯定会有用到一些mixin,在vue3官网中给出了这么一段话:

在 Vue 2 中,mixin 是将部分组件逻辑抽象成可重用块的主要工具。但是,他们有几个问题:Mixin 很容易发生冲突:因为每个 mixin 的 property 都被合并到同一个组件中,所以为了避免 property 名冲突,你仍然需要了解其他每个特性。可重用性是有限的:我们不能向 mixin 传递任何参数来改变它的逻辑,这降低了它们在抽象逻辑方面的灵活性。为了解决这些问题,我们添加了一种通过逻辑关注点组织代码的新方法:组合式API

看到没,又是组合式API,所以把项目中的 mixin 也改为了组合式API,实际该起来也比较暴力,就是把mixin 以函数的形式导出来,函数返回之前mixin中的data和方法等等,然后在组件的setup上调用函数(别忘了把props和context传入),贴个代码片段感受下:

// 导入原mixin中数据/方法
const {
  mpComponentStatus,
  mpLoadPromises,
  mpAllLoadPromisesCollected,
  mpStartWatchInputChange,
  mpPageLoadTriggered,
  mpDrawerContentChanged,
  componentTree,
  dataModel,
  scriptContext,
  businessConfig
} = templateMixin.templateHandle(props, ctx);

在mixin中我们可以通过组合式API来处理生命周期的逻辑和一些数据的响应式。

对比之下,基于函数的 API 让我们可以把每个逻辑任务的代码都整理到一个对应的函数中。当我们发现一个组件变得过大时,我们会将它切分成多个更小的组件;同样地,如果一个组件的 setup() 函数变得很复杂,我们可以将它切分成多个更小的函数。而如果是基于选项,则无法做到这样的切分,因为用 mixin 只会让事情变得更糟糕,基于选项 vs基于函数就好像基于 HTML/CSS/JS 组织代码 vs基于单文件组件来组织代码

关于emit

在vue2中,组件可触发哪些事件是不可被定义的,但是在vue3中,组件可触发的事件可以像props一样被定义,可以像下面这样来定义:

props: {
   pageTitle: {
     type: String,
     default: ''
   }
 },
 emits: [
   'dialogShow'
 ]

关于provide/inject

我们写vue的组件库,很可能使用provide/inject来进行组件间的通信,在我的组件库中也同样将它改为了组合式API的方式,使用这种方式在vue3中两者都只能在当前活动实例的 setup()期间调用,下面贴出改造后的代码片段:

// parent.vue
import { provide, defineComponent } from 'vue';
export default defineComponent({
  setup(props, ctx) {
    provide('root', ctx);
    provide('datas', {
      a: 1
    })
  }
})
// child.vue
import { inject, defineComponent } from 'vue';
export default defineComponent({
  setup(props, ctx) {
    // 第二个参数为默认值(可选)
    const root = inject('root', null);
    const datas = inject('datas', {});
  }
})

关于v-model

在组件库中有自己实现v-model的部分,之前使用vue2的版本是这么实现的:

export default {
  props: {
    value: {
      type: String,
      default: ''
    }
  },
  methods() {
    change() {
      this.$emit('input', 'newValue')
    }
  }
}

那么到了vue3 中v-model的 prop 和事件默认名称已更改:

  • prop: value ----> modelValue
  • event: input ----> update:modelValue
export default {
  props: {
    modelValue: {
      type: String,
      default: ''
    }
  },
  emits: ['update:modelValue'],
  methods() {
    change() {
      this.$emit('update:modelValue', 'newValue') // 在vue2中是this.$emit('input', 'newValue')
    }
  }
}

这块的改动是非兼容的,所以如果组件有自己实现v-model一定要注意!

关于变量的命名

之前的组件库中有使用$开头的变量名,也可以正常使用,但是会报警告,在vue3 中变量名不建议以$_开头。都警告了,还说啥了,直接改吧

关于webpack打包配置的变化

我的vue2版本组件库打包是自己配置的webpack ,升级vue3 后关于打包方面也做了以下的一些调整:

  • VueLoaderPlugin 的引入由 require('vue-loader/lib/plugin') 改为 const { VueLoaderPlugin } = require('vue-loader/dist/index')
  • 2.x中使用的 Vue-loader 需要替换为 vue-loader@next
  • 2.x中使用的 vue-template-compiler 需要替换为 @vue/compiler-sfc
  • 注意vue的运行时,需要设置依赖外部vue的运行时

结语

以上是将vue2组件库升级为 vue3版本的一些改动,总体本着最小化改动的原则,这其中有部分是必须要修改的,也有部分是强烈建议修改的,总体耗时粗略统计 24h (累计时间,仅供参考)。

另外说下编码体验,刚开始写起来可能有些不习惯照vue2,不过基本写完一个组件就会感觉vue3的编码方式还是比较 nice 的,尤其是提供的组合式API 用起来比较舒服,不受调用顺序的限制,可以有条件地被调用,可以更好的按照自己的想法来设计,值的一试~~

参考文档

Vue Function-based API RFC

非兼容的变更

组合式API

vue3迁移指南