閒魚大終端UI元件庫——FishUI建設之路

語言: CN / TW / HK

作者:閒魚技術——灼升

背景

隨著閒魚前端架構的不斷演進,一些關鍵技術設施需要結合業務特徵逐步自建,技術方案也要擁抱社群來提升可擴充套件性。一方面, 閒魚跨端開發框架kun 讓前端開發者使用JS/CSS/HTML即可交付終端頁面,同時兼顧了動態性和高效能,另一方面,前端UI框架也正從集團 rax 逐步轉向社群 React 方案。

在這個大背景下,圍繞 kun 和 web 兩個容器的跨端元件建設也勢在必行,因此 Fish UI 應運而生,它將全面擁抱 react 生態,並藉助 kun 容器的能力,為閒魚終端開發者提供一套高易用性和穩定性的跨端(kun & web)UI元件庫。

FishUI 的最大的技術創新點在於跨端,它通過跨端元件的形式對齊 kun 和 web 兩個容器的體驗標準,讓使用者更加專注於業務邏輯而非容器差異,從而實現為業務開發提效的目標。圍繞著該創新和目標,一些可預知的技術難題也隨之出現,比如元件庫的工程結構如何組織、分端構建如何做、開發規範是什麼樣等等,這些問題將在下文中一一得到解答。

整體設計

分層設計

Fish UI 作為相容 kun 和 web 兩端的元件庫,包含底層的kun容器元件和上層的跨端元件,其分層結構大致如下:

image.png

其中【跨端元件】一層是 Fish UI 最上層的封裝,是抹平 kun 和 web 容器差異的一層,同時也是業務開發者使用頻率最高的一層。

理想情況是【跨端元件】要儘可能依賴底層【w3c規範元件】組合擴充套件而成,要足夠豐富,而【kun 擴充套件元件】一層則希望儘可能做薄,因為其與kun 容器強繫結,是為了解決特定場景問題而產生的,不具備通用性。

本文後續也主要講解圍繞【跨端元件】這一層的相關建設內容。

跨端設計

FishUI 最核心的特點在於【跨端相容 kun 和 h5】,也是與其他元件庫最大的差異點。

通常情況下,業務開發者需要手動判斷環境,分別實現兩個容器對應的程式碼,比如渲染一張圖片的程式碼如下:

image.png

通過 FishUI,我們期望的呼叫方式應該是這樣

image.png

通過對比,可以看出 FishUI 跨端設計的核心功能在於:

  • 自動根據 window.kun 判斷環境,執行對應環境程式碼
  • 自動抹平 kun 和 web 兩端的屬性差異,無需使用者手動處理
  • 對外提供統一的 TS 介面宣告
  • 減少模板程式碼量,提升程式碼簡潔度

因此,FishUI 跨端元件的分端設計大致如下(以 image 為 例):

image.png

技術選型

語言

我們選擇了 react + typescript + moduleCSS 作為元件開發語言。

其中樣式方案我們使用了 moduleCSS,主要原因是為了避免多人協作開發時存在的樣式汙染問題。

雖然 【less/sass + 統一字首】也可以解決樣式汙染問題,比如規定所有元件類名都是 fish-ui-xxx 的格式,其他樣式都在該類名的作用域下。但是由於沒有對類名進行 hash ,導致元件使用者在開發過程中可以通過類名來覆寫元件內部元素樣式。這個行為是一把雙刃劍,好處是元件樣式擴充套件性好,使用者遇到元件樣式不滿足需求,很容易覆寫元件內部樣式。壞處則是元件失去了向後相容的特性,比如使用者自定義了 .button-inner-text 樣式,後來某天 Button 元件內部dom結構變更,或者類名調整了,所有使用者的自定義樣式都會失效,容易造成線上問題。

權衡利弊,最終確定了 moduleCSS 的方案,保證元件內部樣式,不允許使用者隨便侵入,如果有自定義樣式需求,可以通過增加元件屬性,或者 css vars 的方式來給使用者開口子。

目錄結構

採用了 monorepo 的單倉多包管理架構,一個元件對應一個 npm 包。

相比於【多倉多包】和【單倉單包(多元件一個包)】,monorepo 同時具備以下幾點優勢:

  • 使用時,支援按需引入單個 npm 包。
  • 開發時,按照目錄隔離,各包獨立迭代發版,避免中心化的協作方式,同時兼顧一次性改多個元件的開發體驗。
  • 在集團內部搭建場景中,目前只能做到包粒度的 treeShaking,因此更細粒度的多包會比一個大整包會更有載入優勢。

至於單個 package 的結構,我們則直接遵循了集團內 ICE PKG 的規範,它提供了單 React 元件的開發/除錯/構建/發包的全套方案。

image.png

依賴管理

整體選用 pnpm 進行依賴包安裝管理,使用 lerna + nx 進行多包版本管理 & 指令碼執行管理。

包管理方案 pnpm

在 monorepo 場景下,npm/yarn 等包管理器等都有自己的 workspace 規範,對於多包的依賴,可以扁平化提升到最外層的 node_modules 目錄下,解決了依賴重複安裝的問題,但是又帶來了其他的問題:

  • 幻影依賴:由於依賴扁平化到最外層之後,包 A 可以直接 import 包 B 的依賴。
  • 分身依賴:指多包依賴了某個包的多個版本,只有一個版本可以提升到最外層 node_modules 下,其他版本還是會巢狀安裝,導致重複安裝,如下圖中 B 包就被安裝了多次。

image.png

  • 多專案重複安裝:比如某個依賴在 10 個專案中使用,就會有 10 份程式碼被安裝到磁碟中。

以上問題,都可以在 pnpm 中解決:

  • 多專案重複安裝問題:pnpm 會全域性快取已經安裝過的包,然後 硬連結 到專案中的 node_modules 目錄下,避免了重複安裝的問題,且大大提升了安裝速度。
  • 幻影依賴 和 分身依賴問題:pnpm 安裝的依賴並非是扁平化結構,而是通過 軟連結 的形式連結到 node_modules 目錄下的 .pnpm 中,這樣就同時避免了幻影依賴和分身依賴的問題。

包版本方案 lerna + nx

lerna 為比較通用的 monorepo 管理工具,既能進行版本管理,又可以兼顧構建工作流、釋出包等流程方面的管理,已經是業界非常主流的多包管理方案。

鑑於單npm包修改後的版本更新,我們不希望影響到其他未修改的 npm 包的版本,因此我們版本管理策略採用 independent 模式

image.png

此外,lerna 6.x 可以配合 nx 來優化整個工作流程,具體表現為:

  • 快取指令碼結果,避免重複執行,可以顯著提升指令碼執行速率。
  • 基於多包的拓撲結構來執行指令碼,比如 a 包 start 執行前,會自動先執行依賴的 b/c 包的 build 命令。
  • 可以提供圖形化的多包依賴拓撲圖。

image.png

分端構建

分端構建保證了業務側不會載入冗餘程式碼,提升頁面載入體驗。

預設情況下,每個元件都採用了 ICE PKG 自帶的構建方式,使用 rollup + swc 來構建出 esm 產物。

最終一個元件 npm 包的入口檔案 index.js 大致如下:

image.png

由上可以看出,元件本身沒有提供分端構建,而是在執行時通過 window.kun 來區分是 kun 環境,還是 web 環境,從而執行對應環境的程式碼。

這對於極致的載入效能是個不能妥協的巨大問題,因此我們在內部統一的構建工具fish-app中,增加了分端構建的能力,這樣在所有業務場景中使用 Fish UI 元件的時候,最終產物就只會包含特定環境的程式碼。

分端構建的核心邏輯是:

  • 在 webpack 構建過程中,新增自定義 loader,在該 loader 中使用 @babel/parser 分析原始碼 ast,並將 window.kun,轉化為 true 或者 false(取決與是否是 kun 環境)。
  • 然後結合 webpack 的 treeShaking,就會自動剔除 deadCode。

上面的程式碼,在構建 kun 環境程式碼時,經過第一步 babel 的轉化後,就是這樣:

image.png

然後經過第二步 webpack 的 treeShaking 後,變成這樣:

image.png

這樣在特定環境,就只會載入對應環境的程式碼,實現了分端載入。

自動化測試

單元測試是保障元件質量和穩定性的重要一環。

ICE PKG 本身並沒有提供自動化測試相關能力,因此我們基於 React 官方推薦的工具鏈來補充單元測試的部分,使用 jest 作為測試框架,使用 React Testing Library 作為React元件測試工具集。

Enzyme 也是非常流程的 React UI 測試工具,但是我們依舊採用了 React Tesing Library 的主要原因是:React Testing Library 更加推崇站在使用者的視角去測試,Enzyme 則更加帶有開發者視角,從二者的 api 設計上就可以看出一二,React Testing Library 幾乎遮蔽了所有侵入到元件內部實現的api,正如使用者一樣,使用者是不會在乎元件的內部的 props/state 是如何變化的,只有開發者在乎。所以 React Testing Library 成為了我們最終的選擇。

基於以上,一個簡單的元件單元測試,會是如下的效果:

image.png

程式碼規範

程式碼規範在多人協作開發的場景,可以大大提升程式碼的可維護性,從而保證高效響應每一次的業務需求。

我們遵循閒魚前端統一程式碼規範,包括如下:

  • 編碼規範 (包含 HTML/JS/TS/React 等規則)
  • commtlint 規則

這部分也可以參考 f2elint,這是《阿里巴巴前端規約》的配套 Lint 工具,可以為專案一鍵接入規約、一鍵掃描和修復規約問題,保障專案的編碼規範和程式碼質量。

API 設計規範

除了程式碼規範外,我們還約定了元件 API 的設計規範,比如 api 命名風格等,有利於形成統一的使用心智,提升易用性

元件 api 主要包含兩部分:

  • 固定屬性,每個元件都要有的屬性
  • 自定義屬性,元件特有的屬性

固定屬性

保留 className 和 style 屬性,方便樣式使用時擴充套件。

image.png

自定義屬性

image.png

其他 API 設計建議

  • 元件應該具有可組合性(類似 HTML 標籤,具有語義化作用)

image.png

  • 元件的受控與非受控
  • 對於受控屬性,應當提供類似 value + onChange 的組合
  • 對於非受控屬性,應當提供類似 defaultValue 屬性
  • 元件應該具有可擴充套件性
  • 合理使用 children 和 renderFunction
  • 合理使用縮寫
  • 非必要不縮寫,減少誤讀率
  • 但是對於一些常見的縮寫規則可以保留,比如 text4Title,date2String 等。

文件站點

文件站點是展示元件庫的視窗,良好的文件可以提升元件庫整體的易用性。

在 FishUI 中,單個元件的文件產出為 markdown 格式,我們採用 docusaurus 來動態生成元件的文件站點,主要基於以下幾點原因:

  • 基於 React 技術棧,社群外掛多,生態成熟,比如 React/Webpack/Midway 等眾多站點都是基於 docusaurus 構建。
  • 擴充套件性好,支援 mdx,允許在 markdown 中無縫插入 jsx 程式碼,還可以 import 元件。
  • 支援文件搜尋,基於 algolia。
  • 跟集團內部結合比較好,比如
  • 是 ICE PKG 的預設文件方案
  • DEF(集團前端工程基座)中有基於 docusaurus 的 FaaS 靜態站點方案

除了 docusaurus 預設提供的 markdown 渲染能力外,我們還擴充套件瞭如下能力:

  • Playground ,可以編輯元件示例程式碼,並實時預覽
  • 原理是基於 @babel/standalone 來對使用者程式碼實時進行編譯,然後使用 new Function 動態執行編譯後的程式碼。
  • KUN 和 h5 的掃碼預覽能力
  • 原理是分別構建元件示例在 kun 和 h5 下的產物,並生成二維碼注入到原本的 markdown 檔案中。

最終單個元件的文件如下:

image.png

難點和挑戰

隨著元件開發進度不斷推進,也暴露出了開發層面的一些問題:

  • React框架的事件機制不支援在kun標籤上繫結自定義事件,例如不支援在 kun-image 標籤上直接增加 onAppear 屬性這個問題可以手動呼叫 dom 的 addEventLisenter 來繫結,也是目前我們選擇的解決方式,但是這種辦法略微繁瑣和笨拙,後續計劃採用執行時方案,hack 整個 React.createElement,劫持元件的props並動態繫結事件和增加屬性。
  • 跨端的屬性適配問題。kun 和 web 兩者本身就有很多本質差異,一些屬性可以 kun 向 w3c 標準適配來解決,但是有時候強行抹平這些差異將付出很大的開發和維護代價,另外 kun 從一開始就不試圖去達到完備的 w3c 標準,因此我們的跨端元件允許針對 kun 和 web 分開設定屬性,最終能達到一致的效果即可。
  • 跨端的單測問題。基於 jest + jsdom 框架,單測無法覆蓋到 kun 環境,kun 端的元件大多是 kun 標籤的橋接實現,因此這部分單測暫時由客戶端保證,後續計劃前端增加對 kun 環境的 mock 。

除了以上開發層面的問題外,專案推進的最大難點是在閒魚終端融合的初步階段,客戶端和前端同學看待問題的視角存在差異,技術棧也未完全對齊,因此單一元件很難由單一同學獨立負責,導致開發維護和溝通的成本較高。另外為了保證元件庫的標準化能力,需要大量的規範約束和工程能力支撐,比如 api 設計規範、互動視覺規範、程式碼開發規範、統一的腳手架能力、統一的 CI/CD 能力等,這些內容並不是一步到位,而是隨著各方協作者的實踐和反饋而持續迭代完善,這個過程註定會非常曲折。

總結與展望

FishUI 剛剛起航,目前我們產出第一批元件剛剛進入業務試用的階段,FishUI 的引入將使得跨端工程的開發相比之前大大提效,主要體現為:

  • 跨端體驗標準更加統一
  • 跨端的模板程式碼量減少
  • 跨端適配問題變少,開發時間縮減

但是放眼未來,FishUI 還有仍舊很多的工作要做,包括:

  • 加強與視覺互動的聯結
  • 跨端的開發和除錯體驗優化
  • 多前端框架支援
  • 更多的業務場景落地

此外,隨著閒魚終端融合的不斷深入,我們期望 FishUI 能夠成為前端開發者和客戶端開發者學習彼此的實驗場,最終能有更多終端開發者藉助 FishUI 的場景成長起來,並助力 FishUI 更好地向前迭代。總之,FishUI 未來的路還很長,相信終有一天,它將出現在閒魚終端的各個線上場景,真正助力閒魚業務高質量向前發展,敬請期待~