6個例項帶你解讀TinyVue 元件庫跨框架技術

华为云开发者联盟發表於2024-04-26

本文分享自華為雲社群《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

6個例項帶你解讀TinyVue 元件庫跨框架技術

二、為什麼要實現元件庫跨框架呢?

目前,Vue擁有Vue2和Vue3兩大主要分支,它們在開發上並不相容。Vue2還可以進一步細分為2.6及之前的版本和Vue2.7這兩個小分支,其中Vue2.7作為2.6與Vue3之間的過渡版本,在開發上起著橋樑作用。

對於現有專案來講,如果遷移到Vue3,難免存在API及元件功能不同步的情況,因此遷移過程將存在一定的成本及風險。而在當前的Vue生態中,諸如Antdesign和Element等知名元件庫都推出了支援Vue2和Vue3的元件。然而這些官網文件和API卻並不通用,這意味著實際上是提供了兩個獨立的元件庫來實現跨框架支援的。

6個例項帶你解讀TinyVue 元件庫跨框架技術

作為致力於實現跨框架的TinyVue元件庫,旨在實現跨不同版本的Vue框架相容性,其獨特之處在於採用單份原始碼策略,透過智慧編譯技術,能夠同時生成適用於Vue 2.6、2.7版本以及Vue3版本的元件包。這意味著開發者只需維護同一個官方網站,並提供一套標準化的API介面,即可滿足多版本Vue使用者的需求。這種設計有效地減少了TinyVue元件庫的維護成本和未來技術遷移的風險。

三、關鍵技術剖析

首先以一個button元件為例,元件的左上部分是模板,作為元件的入口,它整合了適配層、renderless邏輯以及theme樣式(此處暫不涉及theme部分)。值得注意的是,元件內部並未包含任何邏輯程式碼,所有邏輯均被抽離至renderless中,這裡可以按照下圖所示觀察其呼叫關係。

6個例項帶你解讀TinyVue 元件庫跨框架技術

  • 從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的指令生命週期的名稱變化了, 但指令的引數基本不變

6個例項帶你解讀TinyVue 元件庫跨框架技術

解決方案:在開發指令物件時,透過補齊指令週期,讓指令物件同時支援Vue2 和 Vue3

6個例項帶你解讀TinyVue 元件庫跨框架技術

(6)動畫型別的差異:

6個例項帶你解讀TinyVue 元件庫跨框架技術

解決方案:在全域性的動畫類名檔案中,同時補齊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不支援多根節點元件而統一採用單根節點設計;二是“相容幷包”,透過構建適配層隱藏框架間的差異,提供統一介面,無需開發者手動判斷框架版本,這樣他們可以更專注於邏輯開發。在指令物件和動畫類名等細節方面,同樣貫徹這一簡化差異、廣泛相容的理念。

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章