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