6个实例带你解读TinyVue 组件库跨框架技术

本文分享自华为云社区《6个实例带你解读TinyVue 组件库跨框架技术》,作者: 华为云社区精选。

在DTSE Tech Talk 《手把手教你实现mini版TinyVue组件库》的主题直播中,华为云前端开发DTSE技术布道师阿健老师给开发者们展开了组件库跨框架的讨论,同时针对TinyVue组件库的关键技术进行了剖析,并通过项目实战演示了一份源码编译出2个不同Vue 框架的组件。最后针对框架间的差异,也给出了相应的技术方案,帮助开发者们实战完成组件库跨框架。

直播链接:https://bbs.huaweicloud.com/live/DTT_live/202404171630.html

一、手把手带你实现mini 版 TinyVue

当前实现组件库的跨框架技术,是提升Web页面开发效率与应用灵活性的重要手段。本次直播的实战环节,用300行代码模拟了 TinyVue 组件库的跨框架实现,开发者可以在mini 版组件库中,复现跨框架及多端适配两大功能。同时通过本期的实操环节,也给开发者呈现一个明确且详尽的实现流程,协助大家更好的理解并掌握跨框架技术并运用到实际工作中。

具体源码可参考: https://github.com/opentiny/mini-tiny-vue

二、为什么要实现组件库跨框架呢?

目前,Vue拥有Vue2和Vue3两大主要分支,它们在开发上并不兼容。Vue2还可以进一步细分为2.6及之前的版本和Vue2.7这两个小分支,其中Vue2.7作为2.6与Vue3之间的过渡版本,在开发上起着桥梁作用。

对于现有项目来讲,如果迁移到Vue3,难免存在API及组件功能不同步的情况,因此迁移过程将存在一定的成本及风险。而在当前的Vue生态中,诸如Antdesign和Element等知名组件库都推出了支持Vue2和Vue3的组件。然而这些官网文档和API却并不通用,这意味着实际上是提供了两个独立的组件库来实现跨框架支持的。

作为致力于实现跨框架的TinyVue组件库,旨在实现跨不同版本的Vue框架兼容性,其独特之处在于采用单份源代码策略,通过智能编译技术,能够同时生成适用于Vue 2.6、2.7版本以及Vue3版本的组件包。这意味着开发者只需维护同一个官方网站,并提供一套标准化的API接口,即可满足多版本Vue用户的需求。这种设计有效地减少了TinyVue组件库的维护成本和未来技术迁移的风险。

三、关键技术剖析

首先以一个button组件为例,组件的左上部分是模板,作为组件的入口,它集成了适配层、renderless逻辑以及theme样式(此处暂不涉及theme部分)。值得注意的是,组件内部并未包含任何逻辑代码,所有逻辑均被抽离至renderless中,这里可以按照下图所示观察其调用关系。

  • 从vue文件(即组件的入口文件)开始,引入了适配层中的setup函数和无状态的renderless函数。setup函数的调用过程中,将包含状态的props和context,以及无状态的纯函数renderless一并传入。
  • 然后进入setup函数内部,适配层中的tools函数会构造一个对象,用于抹平框架之间的差异,并将该对象传递给renderless函数。这样,在renderless函数中,可以放心地引用该对象,而无需担心组件是在vue2还是vue3环境下运行。
  • 接下来调用纯函数renderless。它为每个组件构造一个与当前组件相关联的state和api,这些都是有状态的值。随后,这些状态值被返回给适配层。
  • 最后适配层将这些状态值传递给模板进行绑定。具体而言,state被绑定到模板的数据值上,而api则被绑定到模板的事件上。

整体来看,调用过程就像一个管道,数据从模板开始流动,经过逻辑处理,再流回到模板上。在这个过程中,它流经的适配层巧妙地抹平了框架之间的差异,正是TinyVue跨框架的精妙所在。

四、如何解决框架差异统一,实现跨框架?

1、框架间的差异是什么?

Vue3是一次全新的框架升级,所以它的语法以及内部实现,都发生了很大的变化,这些是在开发跨框架组件库时必须考虑的问题。而在长期的跨框架组件库的开发中,可能会遇到众多的框架差异,具体可以将这些差异归结为2大类:

(1)框架对外差异,直接影响到模板的开发以及某些语法。例如:

  • 模板语法差异
  • 生命周期名称变化
  • 移除了事件修饰符、过滤器、消息订阅
  • v-model 语法糖差异
  • 指令,动画组件的差异

(2)框架内部差异,主要是Vue runtime层面的实现差异。在开发跨框架组件过程中,需要访问组件内部某些变量时可能会遇到,例如:

  • 组件实例的差异
  • Vnode结构的差异
  • 移除了$children, $scopedSlots等

2、 框架差异及应对方案

(1)响应式函数引入包差异:

在Vue 2.6 中引入响应函数

复制代码
import { reactive, ref, watch, ... } from '@vue/composition-api'

在Vue 3 中引入响应函数

复制代码
import { reactive, ref, watch, ... } from 'vue'

解决方案:通过在适配层暴露一个hooks变量,统一响应式函数的访问,代码如下

复制代码
// adapter/vue2/index.js

import * as hooks from '@vue/composition-api'
// adapter/vue3/index.js

import * as hooks from 'vue'
// adapter/index.js

export { hooks }

(2)VNode和 h 函数的差异:

在Vue 2.6中,渲染函数的 VNode 参数结构

复制代码
{

  staticClass: 'button',

  class: { 'is-outlined': isOutlined },

  staticStyle: { color: '#34495E' },

  style: { backgroundColor: buttonColor },

  attrs: { id: 'submit' },

  domProps: { innerHTML: '' },

  on: { click: submitForm },

  key: 'submit-button'

}

在Vue 3 中,渲染函数的 VNode 参数结构是扁平的

复制代码
{

  class: ['button', { 'is-outlined': isOutlined }],

  style: [{ color: '#34495E' }, { backgroundColor: buttonColor }],

  id: 'submit',

  innerHTML: '',

  onClick: submitForm,

  key: 'submit-button'

}

解决方案:通过在适配层暴露一个h函数,让Vue3框架也能支持Vue2的参数格式。这样就能统一h 函数的用法,同时让在Vue2时期开发的组件在Vue3框架下兼容运行。

复制代码
// adapter/vue2/index.js

const h = hooks.h
// adapter/vue3/index.js 

const h = (component, propsData, childData) => {

  // 代码有省略...... 

  let props = {}

  let children = childData
  if (propsData && typeof propsData === 'object' && !Array.isArray(propsData)) {

    props = parseProps(propsData)

    propsData.scopedSlots && (children = propsData.scopedSlots)

  } else if (typeof propsData === 'string' || Array.isArray(propsData)) {

    childData = propsData

  }

  return hooks.h(component, props, children)

}
// adapter/index.js

export { h }

(3)v-model的差异:

在Vue 2.6中,在组件上使用 v-model 相当于绑定 value 属性和 input 事件

复制代码
  <ChildComponent v-model="pageTitle" />

  <!-- 会编译为: -->

  <ChildComponent :value="pageTitle" @input="pageTitle = $event" />

在Vue 3 中,v-model 相当于绑定了 modelValue 属性和 update:modelValue 事件

复制代码
  <ChildComponent v-model="pageTitle" />

  <!-- 会编译为: -->

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

解决方案:通过Vue2中声明 model的option 选项,来自定义Vue2框架下v-model 的默认绑定 prop 和 event 。

复制代码
defineComponent({

  model: {

    prop: 'modelValue', // 默认值为 value

    event: 'update:modelValue' // 默认值为 input

  },

  props: {

    modelValue: String

  } // ...

})

(4)slots的差异:

在Vue 2.6中,有普通插槽 slots 和 作用域插槽 scopedSlots

复制代码
// 普通插槽为对象,可以直接使用

this.$slots.mySlot
// 作用域插槽为函数,要按函数来调用

this.$scopedSlots.header()

在Vue 3 中,统一为 slots 函数的形式

复制代码
// 将所有 scopedSlots 替换为 slots

this.$slots.header()
// 将原有 slots 改为函数调用方式

this.$slots.mySlot()

解决方案:通过构建一个vm.$slots属性, 来统一2个框架中,访问slots的访问。

复制代码
// adapter/vue2/index.js

  Object.defineProperties(vm, {

     // ......

    $slots: { get: () => instance.proxy.$scopedSlots },

    $scopedSlots: { get: () => instance.proxy.$scopedSlots },

  })
   // adapter/vue3/index.js

   Object.defineProperties(vm, {

    // ......

    $slots: { get: () => instance.slots },

    $scopedSlots: { get: () => instance.slots },

  })

我们在vm下,还暴露了许多框架runtime层面上的组件属性,用于抹平跨Vue框架的差异。在开发跨框架组件时,要使用vm来访问组件,避免直接访问组件的instance。

复制代码
// 创建一个Vue2 运行时的兼容 vm 对象

const createVm = (vm, _instance) => {

  const instance = _instance.proxy
  Object.defineProperties(vm, {

    $attrs: { get: () => instance.$attrs },

    $listeners: { get: () => instance.$listeners },

    $el: { get: () => instance.$el },

    $parent: { get: () => instance.$parent },

    $children: { get: () => instance.$children },

    $nextTick: { get: () => hooks.nextTick },

    $on: { get: () => instance.$on.bind(instance) },

    $once: { get: () => instance.$once.bind(instance) },

    $off: { get: () => instance.$off.bind(instance) },

    $refs: { get: () => instance.$refs },

    $slots: { get: () => instance.$scopedSlots },

    $scopedSlots: { get: () => instance.$scopedSlots },

    $set: { get: () => instance.$set }

  })
  return vm

}

// 创建一个Vue3 运行时的兼容 vm 对象

const createVm = (vm, instance) => {
  Object.defineProperties(vm, {

    $attrs: { get: () => $attrs },

    $listeners: { get: () => $listeners },

    $el: { get: () => instance.vnode.el },

    $parent: { get: () => instance.parent },

    $children:{get:()=>genChild(instance.subTree)},

    $nextTick: { get: () => hooks.nextTick },

    $on: { get: () => $emitter.on },

    $once: { get: () => $emitter.once },

    $off: { get: () => $emitter.off },

    $refs: { get: () => instance.refs },

    $slots: { get: () => instance.slots },

    $scopedSlots: { get: () => instance.slots },

    $set: { get: () => $set }

  })
  return vm

}

(5)指令的差异:

Vue3的指令生命周期的名称变化了, 但指令的参数基本不变

解决方案:在开发指令对象时,通过补齐指令周期,让指令对象同时支持Vue2 和 Vue3

(6)动画类型的差异:

解决方案:在全局的动画类名文件中,同时补齐2个框架下的类名,让动画类同时支持Vue2 和 Vue3的Transition组件

复制代码
// 此处同时写了 -enter \  -enter-from 的类名,所以它同时支持vue2,vue3的 Transition 组件。

.fade-in-linear-enter,

.fade-in-linear-enter-from,

.fade-in-linear-leave-to {

  opacity: 0;

}

在构建TinyVue跨框架组件库的过程中,团队集中攻克了多个Vue框架间的关键差异点,其中六项尤为突出且具有代表性。

开发TinyVue跨框架组件库时,面对Vue2与Vue3的重要区别,我们确立了两个核心原则:一是"求同去异",即在编写组件时选用两框架都支持的通用语法,如因Vue2不支持多根节点组件而统一采用单根节点设计;二是"兼容并包",通过构建适配层隐藏框架间的差异,提供统一接口,无需开发者手动判断框架版本,这样他们可以更专注于逻辑开发。在指令对象和动画类名等细节方面,同样贯彻这一简化差异、广泛兼容的理念。

点击关注,第一时间了解华为云新鲜技术~

相关推荐
华为云开发者联盟12 分钟前
一文为你深度解析LLaMA2模型架构
大模型·nlp·llama·transformers·华为云开发者联盟
华为云开发者联盟6 小时前
GaussDB(DWS)性能调优,解决DM区大内存占用问题
性能调优·华为云开发者联盟·华为云gaussdb(dws)
华为云开发者联盟1 天前
一文了解Spring Boot启动类SpringApplication
java·spring boot·华为云开发者联盟·springapplication
华为云开发者联盟1 天前
从零开始带你上手体验Sermant自定义插件开发
微服务·云原生·开源·华为云开发者联盟
华为云开发者联盟2 天前
深度解读昇腾CANN多流并行技术,提高硬件资源利用率
cann·华为云开发者联盟·华为云图引擎ges·多流并行
华为云开发者联盟3 天前
探秘数据库中的并行计算技术应用
mysql·并行计算·华为云开发者联盟
Amd7945 天前
Nuxt3 的生命周期和钩子函数(五)
前端框架·生命周期·web开发·服务器渲染·nuxt3·钩子函数·自定义构建
艾恩小灰灰6 天前
HTML5与HTML:不仅仅是标签的革新
前端·html·html5·web开发·web前端·区别·关系
艾恩小灰灰7 天前
探索HTML DOM的奥秘:从基础到实践
前端·html·html5·web开发·前端开发·web前端·dom
华为云开发者联盟7 天前
深入理解Spring AOP中的@EnableAspectJAutoProxy
java·spring·aop·华为云开发者联盟