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不支持多根節點組件而統一採用單根節點設計;二是“兼容幷包”,通過構建適配層隱藏框架間的差異,提供統一接口,無需開發者手動判斷框架版本,這樣他們可以更專注於邏輯開發。在指令對象和動畫類名等細節方面,同樣貫徹這一簡化差異、廣泛兼容的理念。

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章