博客 / 詳情

返回

TinyVue:與 Vue 交往八年的組件庫

本文由體驗技術團隊莫春輝老師原創~

去年因故停辦的 VueConf,今年如約在深圳舉行。作為東道主 & 上屆 VueConf 講師的我,沒有理由不來湊個熱鬧。大會結束後,我見裕波在朋友圈轉發 Jinjiang 的文章《我和 Vue.js 的十年》,我就在下面打趣道:“過兩年我也寫篇同名文章”,然後裕波回覆:“先寫一個我和 Vue 的八週年”。我尋思,我那十分鐘的閃電演講,有人吐槽沒有乾貨,比如同時支持 Vue2 和 Vue3 的技術細節,不如就利用這個機會,給閃電演講打個補丁。

與 Vue 結緣

QCon 2016 上海

時間先回到 2014 年,我剛加入華為公共技術平台部,參與 HAE 前端框架的研發。HAE 的全稱是 Huawei Application Engine,即華為應用引擎。當時我們部門負責集團 IT 系統的基礎設施建設。

作為雲開發平台,HAE 需要支持全面的雲化:雲端開發、雲端測試、雲端部署、雲端運營,以及應用實施的雲化。其中,雲端開發由 Web IDE 負責實現,這個 IDE 為用户提供基於配置的前端開發能力,因此需要支持可配置的 HAE 前端框架。

在 2014 年開始研發 HAE 時,主流的前端技術仍以 jQuery 為主。到了 2016 年,逐漸成熟的 HAE 進入第三個年頭,已支撐內部多個關鍵領域的落地。作為前端架構負責人,我在考察 Angular、React、Vue 三個新興框架,然後選擇其中一個作為 HAE 後續的技術演進方向。於是 10 月份我來到上海,參加 QCon 全球軟件開發大會。那時,尤雨溪給我的第一印象是這樣的:

image.png

這張我用手機抓拍的照片堪稱經典。當時的 Vue 就像他懷裏沒長大的娃,早早展露於舞台之上。選擇 Vue 不是因為它有多強大,而是相信這位奶爸的親和力,能把前端社區凝聚在一起共建 Vue 生態,讓我相信 Vue 會有更亮眼的未來。

VueConf 2017 波蘭

從 QCon 回來後不久,我們提交了 Vue 與 React 的對比分析報告並作了彙報。2017 年初,我們已經開始深入研究 Vue 替換 HAE 底層框架的可行性。因為在 HAE 我們已經實現了 Vue 的能力,比如數據雙向綁定、生命週期管理、路由管理、模塊化管理等等,然後在這個基礎上用面向對象的方式構建了一套企業級組件庫。

使用 Vue 進行重構,就是把底層框架的能力用 Vue 替換,然後上層的組件庫再遷移到 Vue。這個過程相當磨人,中間遇到各種問題,包括 Webpack 構建工具對於當時的我們都是新鮮事物。我們急需專業指導,急需一個信心,然後,首屆 VueConf 就來了。

後來我從裕波那裏得知,首屆 VueConf 是 2017 年 5 月在北京舉辦,而我去的波蘭 VueConf 是 6 月份。但沒關係,當我看到電影院現場座無虛席,一羣來自全世界各個國家的 Vue 開發者匯聚一堂,就已經給了我很大的信心。當時的講師陣容是這樣的:

image.png

會議茶歇期間,我跟同事去找尤雨溪,當他得知我們從國內不遠千里過來參會,很是驚訝。其實,當我瞭解到現場來參會的開發者,大部分都是個人名義自費參加,也讓我驚歎 Vue 的魅力和影響力。

VueConf 2019 荷蘭

經過近半年的研發,HAE 組件庫成功遷移到 Vue 框架,並於 2017 年 12 月正式發佈。在 2018 年,為統一用户體驗遵循 Aurora 主題規範,我們對組件庫進行升級改造,並改名為 AUI(後來又更名為 TinyVue)。在支撐了製造、採購、供應、財經等領域的大型項目後,到了 2019 年 AUI 進入成熟穩定期,我們才有時間去思考如何將 jQuery 的 30 萬行代碼重構為純 Vue 的代碼。

與此同時,Vue 在這兩年裏也得到長足的發展。2019 年 2 月 14 日情人節我再次參加海外的 VueConf,地點已經由波蘭移至荷蘭,從電影院升級成大劇院,舞台變得更豪華,但不變的是尤雨溪的幻燈片,一直保持樸素的風格:

image.png

2019 年 5 月 16 日,VueConf 大會三個月後,美國商務部將華為列入出口管制“實體名單”,我們面臨前所未有的困難,保證業務連續性成為我們首要任務。我們要做最壞的打算,如果有一天所有的主流前端框架 Angular、React、Vue 都不能再繼續使用,那麼重構後的 Vue 代碼又將何去何從?

就在這時 2019 年 5 月 30 日尤雨溪發佈了一個 Vue3 關於 Function API 的 RFC,也就是 Composition API 的前身。Function API RFC 這個鏈接我一直收藏至今,因為它啓發了我如何重新設計組件架構。接下來的內容都是乾貨,想了解 TinyVue 同時支持 Vue2 和 Vue3 的技術細節請往下看。

全新架構的TinyVue組件庫

有了 Function API 的支持,我們組件的核心代碼就可以與前端框架解耦。經過不斷的打磨和完善,擁有全新架構的 TinyVue 組件庫逐漸浮出水面,以下就是 TinyVue 組件的架構圖:

image.png

在這個架構下,TinyVue 組件有統一的 API 接口,開發人員只需寫一份代碼,組件就能支持不同終端的展現,比如 PC 端和 Mobile 端,而且還支持不同的 UX 交互規範。藉助 React 框架的 Hooks API 或者 Vue 框架的 Composition API 可以實現組件的核心邏輯代碼與前端框架解耦,甚至實現一套組件庫代碼,同時支持 Vue 的不同版本。

接下來,我們先分析開發組件庫面臨的問題,再來探討面向邏輯編程與無渲染組件,最後以實現一個 TODO 組件為例,來闡述我們的解決方案,通過示例代碼展現我們架構的四個特性:跨技術棧、跨技術棧版本、跨終端和跨 UX 規範。

其中,跨技術棧版本這個特性,已經為華為內部 IT 帶來巨大的收益。由於 Vue 框架最新的 3.0 版本不能完全向下兼容 2.0 版本,而 2.0 版本又將於 2023 年 12 月 31 日到達生命週期終止(EOL)。於是華為內部 IT 所有基於 Vue 2.0 的應用都必須在這個日期之前升級到 3.0 版本,這涉及到幾千萬行代碼的遷移整改,正因為我們的組件庫同時支持 Vue 2.0 和 3.0,使得這個遷移整改的成本大大降低。

開發組件庫面臨的問題

目前業界的前端 UI 組件庫,一般按其前端框架 Angular、React 和 Vue 的不同來分類,比如 React 組件庫,Angular 組件庫、Vue 組件庫,也可以按面向的終端,比如 PC、Mobile 等不同來分類,比如 PC 組件庫、Mobile 組件庫、小程序組件庫等。兩種分類交叉後,又可分為 React PC 組件庫、React Mobile 組件庫、Angular PC 組件庫、Angular Mobile 組件庫、Vue PC 組件庫、Vue Mobile 組件庫等。

比如阿里的 Ant Design 分為 PC 端:Ant Design of ReactAnt Design of AngularAnt Design of Vue,Mobile 端:Ant Design Mobile of React(官方實現)Ant Design Mobile of Vue(社區實現)。

另外,由於前端框架 Angular、React 和 Vue 的大版本不能向下兼容,導致不同版本對應不同的組件庫。以 Vue 為例,Vue 2.0 和 Vue 3.0 版本不能兼容,因此 Vue 2.0 的 UI 組件庫跟 Vue 3.0 的 UI 組件庫代碼是不同的,即同一個技術棧也有不同版本的 UI 組件庫。

比如阿里的 Ant Design of Vue 其 1.x 版本 for Vue 2.0,而 3.x 版本 for Vue 3.0。再比如餓了麼的 Element 組件庫,Element UI for Vue 2.0,而 Element Plus for Vue 3.0。

我們將上面不同分類的 UI 組件庫彙總在一張圖裏,然後站在組件庫使用者的角度上看,如果要開發一個應用,那麼先要從以下組件庫中挑選一個,然後再學習和掌握該組件庫,可見當前多端多技術棧的組件庫給使用者帶來沉重的學習負擔。

image.png

這些 UI 組件庫由於前端框架不同、面向終端不同,常規的解決方案是:不同的開發人員來開發和維護不同的組件庫,比如需要懂 Vue 的開發人員來開發和維護 Vue 組件庫,需要懂 PC 端交互的開發人員來開發和維護 PC 組件庫等等。

很明顯,這種解決方案首先需要不同技術棧的開發人員,而市面上大多數開發人員只精通一種技術棧,其他技術棧則只是瞭解而已。這樣每個技術棧就得獨立安排一組人員進行開發和維護,成本自然比單一技術棧要高得多。另外,由於同一技術棧的版本升級導致的不兼容,也讓該技術棧的開發人員必須開發和維護不同版本的代碼,使得成本進一步攀升。

面對上述組件開發和維護成本高的問題,業界還有一種解決方案,即以原生 JavaScript 或 Web Component 技術為基礎,構建一套與任何開發框架都無關的組件庫,然後再根據當前開發框架流行的程度,去適配不同的前端框架。比如 Webix 用一套代碼適配任何前端框架,既提供原生 JavaScript 版本的組件庫,也提供 Angular、React 和 Vue 版本的組件庫。

這種解決方案,其實開發難度更大、維護成本更高,因為這相當於先要自研一套前端框架,類似於我們以前的 HAE 框架,然後再用不同的前端框架進行套殼封裝。顯然,套殼封裝勢必影響組件的性能,而且封閉自研的框架其學習門檻、人力成本要高於主流的開源框架。

面向邏輯編程與無渲染組件

當前主流的前端框架為 Angular、React 和 Vue,它們提供兩種不同的開發範式:一種是面向生命週期編程,另一種是面向業務邏輯編程。基於這些前端框架開發應用,頁面上的每個部分都是一個 UI 組件或者實例,而這些實例都是由 JavaScript 創造出來的,都具有創建、掛載、更新、銷燬的生命週期。

所謂面向生命週期編程,是指基於前端框架開發一個 UI 組件時,按照該框架定義的生命週期,將 UI 組件的相關邏輯代碼註冊到指定的生命週期鈎子函數裏。以 Vue 框架的生命週期為例,一個 UI 組件的邏輯代碼可能被拆分到 beforeCreate、created、beforeMount、mounted、beforeUnmount、unmounted 等鈎子函數裏。

所謂面向邏輯編程,是指在前端開發的過程中,尤其在開發大型應用時,為解決面向生命週期編程所引發的問題,提出新的開發範式。以一個文件瀏覽器的 UI 組件為例,這個組件具備以下功能:

  • 追蹤當前文件夾的狀態,展示其內容
  • 處理文件夾的相關操作 (打開、關閉和刷新)
  • 支持創建新文件夾
  • 可以切換到只展示收藏的文件夾
  • 可以開啓對隱藏文件夾的展示
  • 處理當前工作目錄中的變更

假設這個組件按照面向生命週期的方式開發,如果為相同功能的邏輯代碼標上一種顏色,那將會是下圖左邊所示。可以看到,處理相同功能的邏輯代碼被強制拆分在了不同的選項中,位於文件的不同部分。在一個幾百行的大組件中,要讀懂代碼中一個功能的邏輯,需要在文件中反覆上下滾動。另外,如果我們想要將一個功能的邏輯代碼抽取重構到一個可複用的函數中,需要從文件的多個不同部分找到所需的正確片段。

image.png

如果用面向邏輯編程重構這個組件,將會變成上圖右邊所示。可以看到,與同一個功能相關的邏輯代碼被歸為了一組:我們無需再為了一個功能的邏輯代碼在不同的選項塊間來回滾動切換。此外,我們可以很輕鬆地將這一組代碼移動到一個外部文件中,不再需要為了抽象而重新組織代碼,從而大大降低重構成本。

早在 2018 年 10 月,React 推出了 Hooks API,這是一個重要的里程碑,對前端開發人員乃至社區生態都產生了深遠的影響,它改變了前端開發的傳統模式,使得函數式組件成為構建複雜 UI 的首選方式。到了 2019 年初,Vue 在研發 3.0 版本的過程中也參考了 React 的 Hooks API,並且為 Vue 2.0 版本添加了類似功能的 Composition API

當時我們正在規劃新的組件架構,在瞭解 Vue 的 Composition API 後,意識到這個 API 的重要性,它就是我們一直尋找的面向邏輯編程。同時,我們也發現業界有一種新的設計模式 —— 無渲染組件,當我們嘗試將兩者結合在一起,之前面臨的問題隨即迎刃而解。

無渲染組件其實是一種設計模式。假設我們開發一個 Vue 組件,無渲染組件是指這個組件本身並沒有自己的模板(template)以及樣式。它裝載的是各種業務邏輯和狀態,是一個將功能和樣式拆開並針對功能去做封裝的設計模式。這種設計模式的優勢在於:

  • 邏輯與 UI 分離:將邏輯和 UI 分離,使得代碼更易於理解和維護。通過將邏輯處理和數據轉換等任務抽象成無渲染組件,可以將關注點分離,提高代碼的可讀性和可維護性。
  • 提高可重用性:組件的邏輯可以在多個場景中重用。這些組件不依賴於特定的 UI 組件或前端框架,可以獨立於界面進行測試和使用,從而提高代碼的可重用性和可測試性。
  • 符合單一職責原則:這種設計鼓勵遵循單一職責原則,每個組件只負責特定的邏輯或數據處理任務。這樣的設計使得代碼更加模塊化、可擴展和可維護,減少了組件之間的耦合度。
  • 更好的可測試性:由於無渲染組件獨立於 UI 進行測試,可以更容易地編寫單元測試和集成測試。測試可以專注於組件的邏輯和數據轉換,而無需關注界面的渲染和交互細節,提高了測試的效率和可靠性。
  • 提高開發效率:開發人員可以更加專注於業務邏輯和數據處理,而無需關心具體的 UI 渲染細節。這樣可以提高開發效率,減少重複的代碼編寫,同時也為團隊協作提供了更好的可能性。

比如下圖的示例,兩個組件 TagsInput A 和 TagInput B 都有相似的功能,即提供 Tags 標籤錄入、刪除已有標籤兩種能力。雖然它們的外觀截然不同,但是錄入標籤和刪除標籤的業務邏輯是相同的,是可以複用的。無渲染組件的設計模式將組件的邏輯和行為與其外觀展現分離。當組件的邏輯足夠複雜並與它的外觀展現解耦時,這種模式非常有效。

image.png

單純使用面向邏輯的開發範式,僅僅只能讓相同的業務邏輯從原本散落到生命週期各個階段的部分匯聚到一起。無渲染組件的設計模式的實現方式有很多種,比如 React 中可以使用 HOC 高階函數,Vue 中可以使用 scopedSlot 作用域插槽,但當組件業務邏輯日趨複雜時,高階函數和作用域插槽會讓代碼變得難以理解和維護。

要實現組件的核心邏輯代碼與前端框架解耦,實現跨端跨技術棧,需要同時結合面向邏輯的開發範式與無渲染組件的設計模式。首先,按照面向邏輯的開發範式,通過 React 的 Hooks API,或者 Vue 的 Composition API,將與前端框架無關的業務邏輯和狀態拆離成相對獨立的代碼。接着,再使用無渲染組件的設計模式,將組件不同終端的外觀展現,統一連接到已經拆離相對獨立的業務邏輯。

跨端跨技術棧 TODO 組件示例

接下來,我們以開發一個 TODO 組件為例,講解基於新架構的組件如何實現跨端跨技術棧。假設該組件 PC 端的展示效果如下圖所示:

image.png

對應 Mobile 端的展示效果如下圖所示:

image.png

該組件的功能如下:

  • 添加待辦事項:在輸入框輸入待辦事項信息,點擊右邊的 Add 按鈕後,下面待辦事項列表將新增一項剛輸入的事項信息。
  • 刪除待辦事項:在待辦事項列表裏,選擇其中一個事項,點擊右邊的 X 按鈕後,該待辦事項將從列表裏清除。
  • 移動端展示:當屏幕寬度縮小時,組件將自動切換成如下 Mobile 的展示形式,功能仍然保持不變,即輸入內容直接按回車鍵添加事項,點擊 X 刪除事項。

這個 TODO 組件的實現分為 Vue 版本和 React 版本,即支持兩個不同的技術棧。以上特性都複用一套 TODO 組件的邏輯代碼。這套 TODO 組件的邏輯代碼以柯里化函數形式編寫。柯里化(英文叫 Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受餘下的參數且返回結果的新函數的技術。舉一個簡單的例子:

// 普通函數var add = function (x, y) {
  return x + y}add(3, 4) // 返回 7// 柯里化函數var foo = function (x) {
  return function (y) {
    return x + y
  }}foo(3)(4) // 返回 7

本來應該一次傳入兩個參數的 add 函數,柯里化函數變成先傳入 x 參數,返回一個包含 y 參數的函數,最終執行兩次函數調用後返回相同的結果。一般而言,柯里化函數都是返回函數的函數。

回到 TODO 組件,按照無渲染組件的設計模式,首先寫出不包含渲染實現代碼,只包含純業務邏輯代碼的函數,以 TODO 組件的添加和刪除兩個功能為例,如下兩個柯里化函數:

/**
 * 添加一個標籤,給定一個 tag 內容,往已有標籤集合裏添加該 tag
 * 
 * @param {object} text - 輸入框控件綁定數據
 * @param {object} props - 組件屬性對象
 * @param {object} refs - 引用元素的集合
 * @param {function} emit - 拋出事件的方法
 * @param {object} api - 暴露的API對象
 * @returns {boolean} 標籤是否添加成功
 */
const addTag = ({ text, props, refs, emit, api }) => tag => {
  // 判斷 tag 內容是否為字符串,如果不是則取輸入框控件綁定數據的值
  tag = trim(typeof tag === 'string' ? tag : text.value)
  // 檢查已存在的標籤集合裏是否包含新 tag 的內容
  if (api.checkTag({ tags: props.tags, tag })) {
    // 如果已存在則返回添加失敗
    return false
  }
  // 從組件屬性對象獲取標籤集合,往集合裏添加新 tag 元素
  props.tags.push(tag)
  // 清空輸入框控件綁定數據的值
  text.value = ''
  // 從引用元素集合裏找到輸入控件,讓其獲得焦點
  refs.input.focus()
  // 向外拋出事件,告知已添加新標籤
  emit('add', tag)
  // 返回標籤添加成功
  return true
}

/**
 * 移除一個標籤,給定一個 tag 內容,從已有標籤集合裏移除該 tag
 * 
 * @param {object} props - 組件屬性對象
 * @param {object} refs - 引用元素的集合
 * @param {function} emit - 拋出事件的方法
 * @returns {boolean} 標籤是否添加成功
 */
const removeTag = ({ props, refs, emit }) => tag => {
  // 從組件屬性對象獲取標籤集合,在集合裏查找 tag 元素的位置
  const index = props.tags.indexOf(tag)
  // 如果位置不是-1,則表示能在集合裏找到對應的位置
  if (index !== -1) {
    // 從組件屬性對象獲取標籤集合,在集合的相應位置移除該 tag 元素
    props.tags.splice(index, 1)
    // 從引用元素集合裏找到輸入控件,讓其獲得焦點
    refs.input.focus()
    // 向外拋出事件,告知已刪除標籤
    emit('remove', tag)
    // 返回標籤移除成功
    return true
  }
  // 如果找不到則返回刪除失敗
  return false
}

// 向上層暴露業務邏輯方法
export {
  addTag,
  removeTag
}

可以看到這兩個組件的邏輯函數,沒有外部依賴,與技術棧無關。這兩個邏輯函數會被組件的 Vue 和 React 的 Renderless 函數調用。其中 Vue 的 Renderless 函數部分代碼如下:

// Vue適配層,負責承上啓下,即引入下層的業務邏輯方法,自動構造標準的適配函數,提供給上層的模板視圖使用
import { addTag, removeTag, checkTag, focus, inputEvents, mounted } from 'business.js'
/**
 * 無渲染適配函數,根據 Vue 框架的差異性,為業務邏輯方法提供所需的原材料
 * 
 * @param {object} props - 組件屬性對象
 * @param {object} context - 頁面上下文對象
 * @param {function} value - 構造雙向綁定數據的方法
 * @param {function} onMounted - 組件掛載時的方法
 * @param {function} onUpdated - 數據更新時的方法
 * @returns {object} 返回提供給上層模板視圖使用的 API
 */
export const renderless = (props, context, { value, onMounted, onUpdated }) => {
  // 通過頁面上下文對象獲取父節點元素
  const parent = context.parent
  // 通過父節點元素獲取輸入框控件綁定數據
  const text = parent.text
  // 通過父節點元素獲取其上下文對象,再拿到拋出事件的方法
  const emit = parent.$context.emit
  // 通過頁面上下文對象獲取引用元素的集合
  const refs = context.refs
  // 以上為業務邏輯方法提供所需的原材料,基本是固定的,不同框架有所區別
  
  // 初始化輸入框控件綁定數據,如果沒有定義則設置為空字符串
  parent.text = parent.text || value('')

  // 構造返回給上層模板視圖使用的 API 對象
  const api = {
    text,
    checkTag,
    focus: focus(refs),
    // 第一次執行 removeTag({ props, refs, emit }) 返回一個函數,該函數用來給模板視圖的 click 事件
    removeTag: removeTag({ props, refs, emit })
  }

  // 在組件掛載和數據更新時需要處理的方法
  onMounted(mounted(api))
  onUpdated(mounted(api))

  // 與前面定義的 API 對象內容進行合併,新增 addTag 和 inputEvents 方法
  return Object.assign(api, {
    // 第一次執行 addTag({ text, props, refs, emit, api }) 返回一個函數,該函數用來給模板視圖的 click 事件
    addTag: addTag({ text, props, refs, emit, api }),
    inputEvents: inputEvents({ text, api })
  })
}

React 的 Renderless 函數部分代碼如下,這與 Vue 非常類似:

import { addTag, removeTag, checkTag, focus, inputEvents, mounted } from 'business.js'

export const renderless = (props, context, { value, onMounted, onUpdated }) => {
  const text = value('')
  const emit = context.emit
  const refs = context.refs

  const api = {
    text,
    checkTag,
    focus: focus(refs),
    removeTag: removeTag({ props, refs, emit })
  }

  onMounted(mounted(api))
  onUpdated(mounted(api), [context.$mode])

  return Object.assign(api, {
    addTag: addTag({ text, props, refs, emit, api }),
    inputEvents: inputEvents({ text, api })
  })
}

可以看到,TODO 組件的兩個邏輯函數 addTag 和 removeTag 都有被調用,分別返回兩個函數並賦值給 api 對象的兩個同名屬性。而這個技術棧適配層代碼裏的 Renderless 函數,不包含組件邏輯,只用來抹平不同技術棧的差異,其內部按照面向業務邏輯編程的方式,分別調用 React 框架的 Hooks API 與 Vue 框架的 Composition API,這裏要保證組件邏輯 addTag 和 removeTag 的輸入輸出統一。

上述 Vue 和 React 適配層的 Renderless 函數會被與技術棧強相關的 Vue 和 React 組件模板代碼所引用,只有這樣才能充分利用各主流前端框架的能力,避免重複造框架的輪子。以下是 Vue 頁面引用 Vue 適配層 Renderless 函數的代碼:

import { renderless, api } from '../../renderless/Todo/vue'
import { props, setup } from '../common'

export default {
  props: [...props, 'newTag', 'tags'],
  components: {
    TodoTag: () => import('../Tag')
  },
  setup(props, context) {
    return setup({ props, context, renderless, api })
  }
}

React 頁面引用 React 適配層 Renderless 函數,代碼如下所示:

import { useRef } from 'react'
import { renderless, api } from '../../renderless/Todo/react'
import { setup, render, useRefMapToVueRef } from '../common/index'
import pc from './pc'
import mobile from './mobile'
import '../../theme/Todo/index.css'

export default props => {
  const { $mode = 'pc', $template, $renderless, listeners = {}, tags } = props
  const context = {
    $mode,
    $template,
    $renderless,
    listeners
  }

  const ref = useRef()
  useRefMapToVueRef({ context, name: 'input', ref })

  const { addTag, removeTag, inputEvents: { keydown, input }, text: { value } } = setup({ context, props, renderless, api, listeners, $renderless })
  return render({ $mode, $template, pc, mobile })({ addTag, removeTag, value, keydown, input, tags, ref, $mode })
}

至此已完成 TODO 組件支持跨技術棧、複用邏輯代碼。根據無渲染組件的設計模式,前面已經分離組件邏輯,現在還要支持組件不同的外觀。TODO 組件要支持 PC 端和 Mobile 兩種外觀展示,即組件結構支持 PC 端和 Mobile 端。所以我們在 Vue 裏要拆分為兩個頁面文件,分別是 pc.vue 和 mobile.vue,其中 pc.vue 文件裏的 template 組件結構如下:

<template>
  <div align="center">
    <slot name="header"></slot>
    <div align="left" class="max-w-md w-full mx-auto">
      <div class="form-group d-flex">
        <input ref="input" :value="text" :placeholder="newTag" v-on="inputEvents" class="aui-todo aui-font border border-primary shadow-none rounded-0 d-inline todo-input">
        <button class="btn btn-primary shadow-none border-0 rounded-0" @click="addTag">Add</button>
      </div>
      <div class="list-group">
        <div class="list-group-item d-flex justify-content-between align-items-center" v-for="tag in tags" :key="tag">
          <todo-tag :$mode="$mode" :content="tag" />
          <button class="close shadow-none border-0" @click="removeTag(tag)">
            <span>&times;</span>
          </button>
        </div>
      </div>
    </div>
    <slot name="footer"></slot>
  </div>
</template>

 而mobile.vue 文件裏的 template 組件結構如下:

<template>
  <div class="todo-mobile" align="center">
    <slot name="header"></slot>
    <div align="left" class="max-w-md w-full mx-auto">
      <div class="tags-input">
        <span class="tags-input-tag" v-for="tag in tags" :key="tag">
          <todo-tag :$mode="$mode" :content="tag" />
          <button type="button" class="tags-input-remove" @click="removeTag(tag)">&times;</button>
        </span>

        <input ref="input" :value="text" :placeholder="newTag" v-on="inputEvents" class="aui-todo aui-font tags-input-text">
      </div>
    </div>
    <slot name="footer"></slot>
  </div>
</template>

由上可見,PC 端和 Mobile 的組件結構雖然不一樣,但是都引用相同的接口,這些接口就是 TODO 組件邏輯函數輸出的內容。

同理,React 也分為兩個頁面文件,分別是 pc.jsx 和 mobile.jsx,其中 pc.jsx 文件裏的 template 組件結構如下:

import React from 'react'
import Tag from '../Tag'

export default props => {
  const { addTag, removeTag, value, keydown, input, tags, ref, $mode } = props
  return (
    <div align="left" className="max-w-md w-full mx-auto">
      <div className="form-group d-flex">
        <input ref={ref} value={value} onChange={input} onKeyDown={keydown} placeholder="New Tag" type="text" className="aui-todo aui-font border border-primary shadow-none rounded-0 d-inline todo-input" />
        <button className="btn btn-primary shadow-none border-0 rounded-0" onClick={addTag}>Add</button>
      </div>
      <div className="list-group">
        {tags.map(tag => {
          return (
            <div key={tag} className="list-group-item d-flex justify-content-between align-items-center">
              <Tag content={tag} $mode={$mode} />
              <button className="close shadow-none border-0" onClick={() => { removeTag(tag) }}>
                <span>&times;</span>
              </button>
            </div>
          )
        })}
      </div>
    </div >
  )
}

而 mobile.jsx 文件裏的 template 組件結構如下:

import React from 'react'
import Tag from '../Tag'
import '../../style/mobile.scss'

export default props => {
  const { removeTag, value, keydown, input, tags, ref, $mode } = props
  return (
    <div className="todo-mobile" align="center">
      <div align="left" className="max-w-md w-full mx-auto">
        <div className="tags-input">
          {tags.map(tag => {
            return (
              <span key={tag} className="tags-input-tag" >
                <Tag content={tag} $mode={$mode} />
                <button type="button" className="tags-input-remove" onClick={() => { removeTag(tag) }}>&times;</button>
              </span >
            )
          })}
          <input ref={ref} value={value} onChange={input} onKeyDown={keydown} placeholder="New Tag" className="aui-todo aui-font tags-input-text" />
        </div>
      </div>
    </div>
  )
}

由上可見,Vue 和 React 的 PC 端及 Mobile 端的結構基本一樣,主要是 Vue 和 React 的語法區別,因此同時開發和維護 Vue 和 React 組件結構的成本並不高。以下是 TODO 組件示例的全景圖:

image.png

回顧一下我們開發這個 TODO 組件的步驟,主要分為三步:

  • 按無渲染組件的設計模式,首先要將組件的邏輯分離成與技術棧無關的柯里化函數。
  • 在定義組件的時候,藉助面向邏輯編程的 API,比如 React 框架的 Hooks API、Vue 框架的 Composition API,將組件外觀與組件邏輯完全解耦。
  • 按不同終端編寫對應的組件模板,再利用前端框架提供的動態組件,實現動態切換不同組件模板,從而滿足不同外觀的展示需求。

總結

Vue 是開源的,與 Vue 交往的 TinyVue 也應該是開源的。但從自研走向開源的過程並不是一帆風順,從最初的想法到最終落地,前後花了五年多。2023 年初,TinyVue 終於藉助我們的開源網站(https://opentiny.design/) 正式與大家見面。回想這八年對 Vue 的堅持,也是自己對前端框架的堅持。有些人好奇為什麼一個 TinyVue 能做這麼長時間,那是因為最初正確的選擇,加上不懈的創新和努力,讓 TinyVue 紮根於華為內部 IT 業務,支撐幾千萬行的前端業務代碼,才會有如此長久的生命力。

關於 OpenTiny

image.png

OpenTiny 是一套企業級 Web 前端開發解決方案,提供跨端、跨框架、跨版本的 TinyVue 組件庫,包含基於 Angular+TypeScript 的 TinyNG 組件庫,擁有靈活擴展的低代碼引擎 TinyEngine,具備主題配置系統TinyTheme / 中後台模板 TinyPro/ TinyCLI 命令行等豐富的效率提升工具,可幫助開發者高效開發 Web 應用。

歡迎加入 OpenTiny 開源社區。添加微信小助手:opentiny-official 一起參與交流前端技術~
OpenTiny 官網:https://opentiny.design/
OpenTiny 代碼倉庫:https://github.com/opentiny/
TinyVue 源碼:https://github.com/opentiny/tiny-vue
TinyEngine 源碼: https://github.com/opentiny/tiny-engine
歡迎進入代碼倉庫 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI~ 如果你也想要共建,可以進入代碼倉庫,找到 good first issue標籤,一起參與開源貢獻~

user avatar kenx_5e23c96d15b1d 頭像 xiaohuoche 頭像
2 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.