直播回顾|6个实例带你解读TinyVue 组件库跨框架技术

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

直播链接:bbs.huaweicloud.com/live/DTT_li...

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

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

具体源码可参考: github.com/opentiny/mi...

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

目前,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结构的差异
  • 移除了 <math xmlns="http://www.w3.org/1998/Math/MathML"> c h i l d r e n , children, </math>children,scopedSlots等

2、 框架差异及应对方案

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

在Vue 2.6 中引入响应函数

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

在Vue 3 中引入响应函数

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

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

js 复制代码
// adapter/vue2/index.js
import * as hooks from '@vue/composition-api'
// adapter/vue3/index.js
import * as hooks from 'vue'
// adapter/index.jsexport { hooks }

(2)VNode和 h 函数的差异:在Vue 2.6中,渲染函数的 VNode 参数结构

js 复制代码
{  
  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 参数结构是扁平的

js 复制代码
{  
  class: ['button', { 'is-outlined': isOutlined }],
  style: [{ color: '#34495E' }, 
  { backgroundColor: buttonColor }],  
  id: 'submit',  innerHTML: '',  
  onClick: submitForm,  
  key: 'submit-button'
}

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

js 复制代码
// 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 事件

js 复制代码
  <ChildComponent v-model="pageTitle" /> 
  <!-- 会编译为:--> 
  <ChildComponent :value="pageTitle" @input="pageTitle = $event" />

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

js 复制代码
  <ChildComponent v-model="pageTitle" />  
  <!-- 会编译为:-->
  <ChildComponent :modelValue="pageTitle" @update:modelValue="pageTitle = $event" />

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

js 复制代码
defineComponent({ 
  model: {   
    prop: 'modelValue', // 默认值为 value   
    event: 'update:modelValue' // 默认值为 input  
  },  
  props: {    
    modelValue: String 
  } // ...
})

(4)slots的差异: 在Vue 2.6中,有普通插槽 slots 和 作用域插槽 scopedSlots

js 复制代码
// 普通插槽为对象,可以直接使用
this.$slots.mySlot

// 作用域插槽为函数,要按函数来调用
this.$scopedSlots.header()

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

kotlin 复制代码
// 将所有 scopedSlots 替换为 slots
this.$slots.header() 

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

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

js 复制代码
// 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。

js 复制代码
// 创建一个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组件

js 复制代码
// 此处同时写了 -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不支持多根节点组件而统一采用单根节点设计;二是"兼容并包",通过构建适配层隐藏框架间的差异,提供统一接口,无需开发者手动判断框架版本,这样他们可以更专注于逻辑开发。在指令对象和动画类名等细节方面,同样贯彻这一简化差异、广泛兼容的理念。

关于 OpenTiny

OpenTiny 是一套企业级 Web 前端开发解决方案,提供跨端、跨框架、跨版本的 TinyVue 组件库,包含基于 Angular+TypeScript 的 TinyNG 组件库,拥有灵活扩展的低代码引擎 TinyEngine,具备主题配置系统TinyTheme / 中后台模板TinyPro/ TinyCLI命令行等丰富的效率提升工具,可帮助开发者高效开发 Web 应用。


欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~更多视频内容也可关注B站、抖音、小红书、视频号

OpenTiny 也在持续招募贡献者,欢迎一起共建

OpenTiny 官网opentiny.design/
OpenTiny 代码仓库github.com/opentiny/
TinyVue 源码github.com/opentiny/ti...
TinyEngine 源码github.com/opentiny/ti...

欢迎进入代码仓库 Star🌟TinyEngineTinyVueTinyNGTinyCLI~ 如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~

相关推荐
小村儿1 天前
给 AI Agent 装上"长期记忆":Karpathy 的 LLM Wiki 思想,我做成了工具
前端·后端·ai编程
竹林8181 天前
用ethers.js连接MetaMask实现Web3钱包登录:从踩坑到稳定运行的完整记录
前端·javascript
heyCHEEMS1 天前
如何用 Recast 实现静态配置文件源码级读写
前端·node.js
心连欣1 天前
从零开始,学习所有指令!
前端·javascript·vue.js
review445431 天前
大模型和function calling分别是如何工作的
前端
东东同学1 天前
耗时一个月,我把 Nuxt 首屏性能排障经验做成了一个 AI Skill
前端·agent
冴羽1 天前
超越 Vibe Coding —— AI 辅助编程指南
前端·ai编程·vibecoding
梦想的颜色1 天前
一天一个SKILL——前端最佳自动化测试 webapp-testing
前端·web app
SoaringHeart1 天前
Flutter进阶:放弃 MediaQuery.of(context) 使用 NScreenManager
前端·flutter
openKaka_1 天前
从 scheduleUpdateOnFiber 到 Root 微任务调度:React 如何把更新交给调度系统
开发语言·前端·javascript