本文分享自華為雲社群《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不支援多根節點元件而統一採用單根節點設計;二是“相容幷包”,透過構建適配層隱藏框架間的差異,提供統一介面,無需開發者手動判斷框架版本,這樣他們可以更專注於邏輯開發。在指令物件和動畫類名等細節方面,同樣貫徹這一簡化差異、廣泛相容的理念。
點選關注,第一時間瞭解華為雲新鮮技術~