美團React Native開源元件庫beeshell詳解

xiangzhihong發表於2018-10-02

近年來,伴隨著大前端概念的提出和興起,移動端和前端的邊界變得越來越模糊,湧現了一大批移動跨平臺開發框架和模式。從早期的PhoneGap、inoc等Hybird技術,到現在耳熟能詳的React Native、Weex和Flutter等技術,無不體現著移動端開發的前端化。而提供一套三端統一的開發框架,一直是前端奮鬥的目標,而React Native就是這麼一個不錯的三端統一的跨平臺開發框架,這方面的知識可以參考我之前出的《React Native移動開發實戰》關於跨平臺相關的內容分析。

注:本文原文地址開源的 React Native 元件庫

beeshell簡介

一個 React Native 應用的基礎元件庫,基於 0.53.3 版本,提供一整套開箱即用的高質量元件,包含 JS 元件和複合元件(包含 Native 程式碼),涉及 FE、iOS、Android 三端技術,兼顧通用性和定製化,支援自定義主題,用於開發和服務企業級移動應用。開源地址:github.com/meituan/bee…

特性

據稱,beeshell已經被廣泛使用在美團外賣的多條業務線,通過了各種業務場景、作業系統、機型的實戰考驗,具備很好的穩定性、安全性和易用性等特點,基於此,美團將此開源出來以供大家使用和借鑑。

  • UI 樣式的一致性和定製化。
  • 通用性:主要使用 JS 來實現,保證跨平臺通用性。
  • 定製化:我們在比較細的粒度上對元件進行拆分,通過繼承的方式層層依賴,功能漸進式增強,為在任意層級上的繼承擴充套件、個性化定製提供了可能。
  • 原生功能支援:元件庫中的複合元件包含 Native 程式碼,支援圖片選擇、定位等原生功能。
  • 功能豐富:不僅僅提供元件,還提供了基礎工具、動畫以及 UI 規範。 完善的文件和使用示例。

對比

在beeshell開源之前,React Native社群已經出現了很多流行且著名的腳手架框架。此處選取 Github Star 數 5000 以上的元件庫,並從元件數量、通用性、定製化、是否包含原生功能、文件完善程度五個維度來進行對比分析。

元件庫 元件數量 通用性 定製化 是否包含原生功能 文件完善程度
react-native-elements 16 強,提供一套風格一致的 UI 控制元件 弱,若要定製化可能需要重寫
NativeBase 28 強,提供一套風格一致的 UI 控制元件 中,支援主題變數
ant-design-mobile 41 強,提供一套風格一致的 UI 控制元件 中,部分可以支援定製化需求
beeshell 25 強,提供一套風格一致的 UI 控制元件 強,不僅支援主題變數,還支援使用繼承的方式進行定製化擴充套件

通過對比可以看出,beeshell 只在元件數量上稍有劣勢,在其他方面都一致或者優於其他專案。因為 beeshell 具備了良好的系統架構,所以豐富元件數量只時間問題,而且我們團隊也已經有了詳細的規劃來完善數量上的不足。

系統設計

系統設計是將一個實際問題轉換成相應解決方案的主動過程,是解決辦法的描述。在通用的軟體工程模型中,需求分析完成後的第一步就是系統設計。一個專案最終的穩定性、易用性在很大程度上也取決於系統設計這一步。

beeshell 元件庫是為了更加快速的搭建移動端應用,為業務開發提供基礎技術支援,大幅提升開發人效。然而,面對不同的業務方、不同的功能需求、不同的 UI 規範與互動方式,如何有效的兼顧所有的需求?這對系統設計提出了更高的要求,下面以抽象層次逐層降低的方式來詳細介紹 beeshell 的系統設計。

框架設計

beeshell 元件庫基於 React Native,向下通過 React Native 與 iOS、Android 平臺進行系統層面的互動,向上提供開發者友好的統一介面,抹平平臺差異,為使用者開發業務功能提供服務支援。beeshell 扮演了一箇中間者的角色,從而保證了移動端應用基礎功能的穩定性、易用性。其框架的設計原理如下圖。

在這裡插入圖片描述

方案設計

為了更好的介紹beeshell,我們來看一下beeshell設計上的一些細節。整體上使用 JS 作為統一入口,多層封裝隱藏實現細節,抹平 JS 與 Native、iOS 平臺與 Android 平臺的差異,開箱即用,降低了使用者的學習和使用成本。區域性上基於 React Native 的技術特點,分成 JS 元件部分和複合元件部分,兩部分推行“鬆耦合”的開發模式,使得 Native 部分擁有替換變更的能力,提升元件庫的靈活性。

在這裡插入圖片描述

複合元件部分可以直接暴露 JS 介面,如果有需要,也可以在 JS 元件部分進行定製化封裝。我們儘量保證 Native 部分功能的原子性、簡潔性,有任何定製化需求都使用 JS 來統一實現,遵循 JS 實現優先的設計原則,保證跨平臺通用的特性。為了達到上面的要求,下面從JS 元件部分和複合元件兩個部分來介紹。

JS 元件部分設計

一個軟體的設計分為三個設計層次:體系結構、程式碼設計和可執行設計。我們使用自上而下的方法,從體系結構開始進行 JS 元件部分的設計。

軟體的體系結構的風格通常有 7 種:管道和過濾器,物件導向,隱式請求,層次化,知識庫,解釋程式和過程控制。

JS 元件部分使用了層次化的體系結構風格,整體分成三層:基礎工具、通用元件、擴充套件元件,從上到下通用性逐漸減弱、定製化逐漸增強,功能漸進式增強,通過分層設計,各層各司其職,兼顧通用性和定製化。

在這裡插入圖片描述

  • 基礎工具(common):最基礎的、通用的部分,包含 JS Utils、動畫定義、UI 規範等。
  • 通用元件(components):把功能相似的元件進行歸類,整理成一個個系列,每個系列內部使用繼承的方式實現,層層依賴,功能漸進式增強,該部分專注通用性,不考慮定製化需求,保證程式碼的簡潔性。同時,在比較細的粒度對元件進行拆分,提供了良好的可擴充套件性。
  • 擴充套件元件(modules):是對通用元件的繼承擴充套件、組合應用,該部分專注定製化,在最大程度上滿足業務上的需求,通用性較低。

我們擴充套件元件部分會提供大量的定製化元件,如果仍然不能滿足需求,使用者就可以借鑑擴充套件元件的實現,根據自己業務需求,在某一繼承層級上繼承通用元件,自行進行定製化擴充套件,這點充分體現了 beeshell 定製化的能力。

複合元件設計

既然是 React Native 元件庫當然少不了 Native 部分,複合元件包含 Native 的功能。beeshell 元件庫已經完成了 Native 部分的整合方案與規範,有良好的開發與使用體驗,可以不斷的整合原生功能。

在這裡插入圖片描述

複合元件部分通過 JS 封裝介面,保證了跨平臺。Native 部分主要分成 Native Bridge 和純 Native 兩大部分,Bridge 是針對 React Native 的封裝,必須在元件庫中實現;而純 Native 部分則可以通過 Pods/Gradle 依賴三方實現,有效的吸收利用原生開發的技術積累。

元件庫實現

跨平臺元件

React Native 提供了一些內建元件,我們能使用 JS 來實現功能都是基於這些內建元件,這些內建的元件一些是跨平臺通用的元件,如:View、Text、TextInput;而另一些是兩個平臺分別實現的,如 DatePickerIOS 和 DatePickerAndroid、AlertIOS 和 ToastAndroid。例如:

iOS 平臺的 DatePickerIOS 元件:

在這裡插入圖片描述

Android 平臺的 DatePickerAndroid 元件:

在這裡插入圖片描述

不僅功能互動完全不同,而且類名、呼叫方式各異,這不僅滿足不了業務需求,而且也有很高的學習和使用成本。這樣類似的元件還有很多,如何抹平平臺的差異,實現跨平臺?我們提出的方案是優先使用 JS 來實現功能,這也是我們元件庫的設計原則。針對上面的問題我們開發了基於 ScrollView 的 Datepicker 元件,統一類名與呼叫方式,保證了跨平臺通用性。下面是Android和iOS實現的Datepicker元件。

在這裡插入圖片描述
在這裡插入圖片描述

Datepicker 是使用 JS 完全實現了一個完整功能,但是有的情況不需要實現完整的功能,我們可以通過 React Native 提供的 Platform 來進行區域性的跨平臺處理。例如 TextInput 元件,預設在Android平臺下是沒有清空按鈕的,但是我們可以通過自定義來實現清空功能。

定製化支援

隨著移動網際網路的快速發展,各類移動端產品湧現並且不斷髮展,這也讓軟體知識不斷被普及,業務方對產品功能的定位逐漸從廠商主導轉變為使用者主導。產品功能更加精準,個性化、細化、深化是必然趨勢,通過定製化服務來滿足產品發展的要求也應運而生。不同行業、不同型別的產品,功能、特點各不相同,用某一種既定的軟體產品來滿足不同型別的需求,其適用性可想而知。定製化有良好的技術架構和技術優勢,可定製、可擴充套件、可整合、跨平臺,在個性化需求的處理方面,有著很好的優勢,所以我們需要定製化。

樣式定製化

在這裡插入圖片描述

在元件庫設計之初,就已經統一好了 UI 規範。我們根據 UI 規範,統一定義樣式變數並放置在基礎工具層中,即 beeshell/common/styles/varibles.js 檔案中,在 React Native 應用中,樣式變數其實就是普通的 JS 變數,可以很方便的進行復用與重寫操作。React Native 提供了 StyleSheet 通過建立一個樣式表,使用 ID 來引用樣式,減少頻繁建立新的樣式物件,在元件庫的樣式變數應用中靈活使用 StyleSheet.create 和 StyleSheet.flatten 來獲取樣式 ID 和樣式物件。

在每個組的實現中,會事先引入基礎工具層中的樣式變數,使用統一的變數物件而不是在元件中自行定義,這樣就保證了 UI 樣式的一致性。同時,beeshell 提供了重置樣式變數的 API,可以實現一鍵換膚。我們推薦 beeshell 的使用者在開發移動應用時,事先定義好樣式變數。一方面使用自己的樣式變數重置 beeshell 的樣式變數;另一方面在業務功能開發時,使用自己定義好的樣式變數,從而保證整體 UI 的一致性。

功能定製化

樣式定製化可以從巨集觀和整體的角度來實現,而功能的定製化則需要具體問題具體分析,從微觀和區域性的角度來分析和實現。下文將以 Modal 系列的實現為例,來詳細介紹功能定製化。

在移動端的彈窗互動,與 PC 端相比一般會比較簡單,我們把模態框、下拉選單、資訊提示等互動類似的元件統一歸類為 Modal 系列,使用繼承的方式實現。有人可能會問為什麼使用繼承而不用使用組合?前文已經講過,組合的主要目的是程式碼複用,而繼承的主要目的是擴充套件。考慮到彈窗互動有很多定製化的可能性,為了滿足更好的擴充套件性,我們選擇了繼承的方式來實現。下面來看一下實現效果:

在這裡插入圖片描述

提供了遮罩、彈出容器以及淡入淡出(Fade)動畫效果,彈出內容部分完全由使用者自定義。這個元件通用性極強,沒有任何定製化的功能。這裡需要說明下,動畫部分獨立實現,提供了 FadeAnimated 和 SlideAnimated 兩個子類,使用了策略模式與 Modal 系列整合,Modal 元件預設整合 FadeAnimated。

在這裡插入圖片描述

繼承 Modal 元件,對彈出內容做了一定程度的定製化擴充套件,支援標題、確認按鈕、取消按鈕以及自定義 body 部分的功能,通用性減弱,定製化增強。

在這裡插入圖片描述
PageModal繼承 SlideModal 元件,對彈出內容做了定製化擴充套件,支援標題、確認按鈕、取消按鈕以及自定義 body 功能,通用性減弱,定製化增強。

在這裡插入圖片描述
CheckboxModal 元件由 PageModal 和 Checkbox 兩個元件使用組合的方式實現,基於通用型元件組合出了更加強大功能,遵循繼承與組合靈活運用的設計原則。

通過以上部分,我們已經對 Modal 系列已經有了直觀的認識,然後我們來看下 Modal 系列的類圖以及分層。

在這裡插入圖片描述
動畫部分在基礎工具(common)中實現;在通用元件(components)中 Modal 元件聚合 FadeAnimated 動畫,同時因為 SlideModal、ConfirmModal 比較通用,也在該部分實現;CheckboxModal 則定製化比較強,歸類到擴充套件元件(modules)中。通過這種方式的分層,三層各司其職,使得元件庫的層次結構更加清晰,不僅實現了定製化,還保證了通用部分的簡潔性和可維護性。

複雜情況

遞迴處理非同步渲染

React Native 應用的 JS 執行緒和 UI 執行緒是兩個執行緒,與瀏覽器中共用一個執行緒的實現不同,所以我們可以看到 React Native 提供的操作 UI 元素的 API,都是通過回撥函式的方式進行呼叫。

受益於 React,我們一般不需要直接操作 UI 元素,但是有的元件確實需要複雜的 UI 操作,例如完全由 JS 實現的 Scrollerpicker 元件。

在這裡插入圖片描述
在此種Case中,我們需要精確的計算容器以及每一項元素的高度,才能正確得到當前選中的項在資料模型(陣列)中的索引。現在面臨的問題是:在元件渲染完成後的生命週期 componentDidMount 並不能拿到正確容器的高度為,而使用 setTimeout 也會有延遲時長設定為多少的問題。我們選擇使用遞迴來解決,一次 setTimeout 不行就執行多次。
在這裡插入圖片描述

UI 尺寸容錯機制

React Native 為使用者提供了 style 屬性來控制元素的樣式,我們可以手動設定相關 UI 元素的尺寸。但是,在一些 Android 機器上,我們設定的元素尺寸與 measure 方法獲取的尺寸資訊不一致,經過大量 Android 機器的實際的測試,我們得到的結論是:有零點幾畫素的誤差。

在這裡插入圖片描述
可以通過 measure 方法得到尺寸資訊進行向上與向下取整,得到一個閾值範圍,手動設定的尺寸資訊只要在這個閾值範圍內,就認為是有效尺寸,這種容錯機制有效的相容了極端情況,提高了元件的穩定性。

精細化佈局控制

在使用 Form 元件時,最常見的需求就是校驗功能,通常元件庫的 Form 元件都會內建校驗功能。然而,因為校驗方式有同步與非同步兩種,校驗結果展示的樣式、位置五花八門,這就導致了校驗功能的複雜度變得很高。

為此,beeshell提供了以下幾種佈局方式: 絕對定位

在這裡插入圖片描述
Static 定位
在這裡插入圖片描述
自定義位置
在這裡插入圖片描述
如何有效的兼顧不同的需求?我們提出了校驗獨立實現的方式,在使用 Form 元件的父元件中,使用 CVD 來定義、配置校驗規則,校驗結果輸出到統一的資料結構(單一資料來源),基於這個資料結構,我們就能在任意時機、任意位置、使用任意樣式來展示校驗資訊。

何為CVD,下面看一個模型。

在這裡插入圖片描述
CVD 是一個針對複雜表單錄入場景的分層解決方案,輕量級、跨平臺、易擴充套件,內建在 beeshell 元件庫中,可以直接使用。CVD 把表單某個控制元件的錄入的流程分成三層:

  • Connector 聯結器,把使用者輸入的資訊轉化成所需的資料格式。
  • Validator 校驗器,對格式化的資料進行校驗。
  • Dependency 依賴處理器,處理當前控制元件與其他控制元件的依賴關係。

每一層都對單一資料來源 Store 進行不可變資料更新,符合互動內聚和順序內聚,內聚程度高。 每一層使用函式式組合的方式,定義 key(表單控制元件的唯一標誌)與 key 對應的回撥函式,避免了批量 if else,可以有效降低程式的圓環複雜度。

下面以 Input 元件錄入姓名為例,來具體說明CVD的運作原理。

在這裡插入圖片描述
人挪活在 onChange 中獲取使用者輸入,呼叫 cvd.flow 然後就可以通過 cvd.getStore 獲取到結果。
在這裡插入圖片描述
通過校驗功能獨立實現,把校驗資訊輸出到 Store 中,在需要的時候從 Store 中獲取校驗資訊,可以更加精細化的控制元素的樣式、位置與佈局,相容各種定製化需求。

測試

程式碼的終極目標有兩個,第一個是實現需求,第二個是提高程式碼質量和可維護性,測試是為了提高程式碼質量和可維護性,檢測程式碼的質量。

單元測試

單元測試(Unit Testing),是指對軟體中的最小可測試單元進行檢查和驗證。在結構化程式設計的時代,單元測試中單元指的就是函式。beeshell 元件庫全面使用單元測試,由元件的開發者完成。研究成果表明,無論什麼時候作出修改都需要進行完整的迴歸測試,對於提供基礎功能的元件來說更是如此,在生命週期中儘早地對軟體產品進行測試將使效率和質量都得到最好的保證。Bug 發現的越晚,修改它所需的成本就越高,單元測試是一個在早期抓住 Bug 的機會。beeshell 元件庫使用 Jest 做為單元測試的工具,自帶斷言、測試覆蓋率工具,實現開箱即用。

測試用例設計

測試用例的核心是輸入資料,我們會選擇具有代表性的資料作為輸入資料,主要有三種:正常輸入,邊界輸入,非法輸入。下面以元件庫中提供的 isLeapYear 工具函式來舉例說明。

在這裡插入圖片描述
Jest 使用 test 函式來描述一個測試用例,其中的 toBe 邊是一句斷言。

函式使用了外部資料,正常輸入肯定會有,這裡的 2000 和 '2000' 都是正常輸入;邊界輸入和非法輸入並不是所有的函式都有,這裡為了說明使用了有這兩種輸入的例子,邊界輸入是有效輸入的極限值,這裡 0 和 Infinity 是邊界輸入;非法輸入是正常取值範圍以外的資料, 'xx' 和 false 則是非法輸入。一般情況下,考慮以上三種輸入可以找出函式的基本功能點,單元測試與程式碼編寫是“一體兩面”的關係,編碼時對上述三種輸入都是應該考慮的,否則程式碼的健壯性就會出現問題。

上文所說的測試是針對程式的功能來設計的,就是所謂的“黑盒測試”。單元測試還需要從另一個角度來設計測試資料,即針對程式的邏輯結構來設計測試用例,就是所謂的“白盒測試”。

在這裡插入圖片描述
這裡有一個 if else 語句,如果我們只提供一個 2000 的輸入,只會測試到 if 語句,而不會測試 else 語句。雖然,在黑盒測試足夠充分的情況下,白盒測試沒有必要,可惜“足夠充分”只是一種理想狀態,難於衡量測試的完整性是黑盒測試的主要缺陷。而白盒測試恰恰具有易於衡量測試完整性的優點,兩者之間具有極好的互補性。

白盒測試也是比較常見的需求,Jest 內建了測試覆蓋率工具,可以直接在命令中新增 --coverage 引數便可以輸出單元測試覆蓋率的報告。

在這裡插入圖片描述

UI 自動化測試

想要確保元件庫的 UI 不會意外被更改,快照測試(Snapshot Testing)是非常有用的工具。一個典型的移動 App 快照測試案例過程是,先渲染 UI 元件,然後截圖,最後和獨立於測試儲存的參考影象進行比較。

使用 Jest 進行在快照測試,在 beeshell 中第一次對某個元件進行測試時,會在測試目錄下建立一個 snapshots 資料夾,並將快照結果存放在該資料夾中。快照結果檔案以 <元件名>.js.snap 命名,其內容為某個狀態下的 UI 元件樹。下面以 Button 元件為例來說明如何使用快照測試。

在這裡插入圖片描述

執行命令後得到快照結果如下:

在這裡插入圖片描述

靜態分析

經常與單元測試聯絡起來的開發活動還有靜態分析(Static analysis)。靜態分析就是對軟體的原始碼進行研讀,查詢錯誤或收集一些度量資料,並不需要對程式碼進行編譯和執行。

靜態分析效果較好而且快速,可以發現 30%~70% 的程式碼問題,可以在幾分鐘內檢查一遍,成本低、收益高。beeshell 使用 SonarQube 進行靜態程式碼檢查。

SonarQube 是一個開源的程式碼質量管理系統,支援 25+ 種語言,可以通過使用外掛機制與 Eclipse、VSCode 等工具整合,實現對程式碼的質量的全面自動化分析和管理。

SonarQube 通過對 Reliability(可靠性)、Security(安全性)、Maintainability(可維護性)、Coverage(測試覆蓋率)、Duplications(重複)幾個維度,對程式碼進行全方位的分析,通過設定 Quality Gates 保證程式碼質量。詳細的使用情況可以訪問SonarQube官網文件

beeshell開發與使用

beeshell 元件庫使用 npm 包的形式下載使用,下載成功後會放置在專案根目錄的 node_modules 目錄,然後在專案中通過引入模組的方式,引入 beeshell 的元件來使用。

那我們如何開發元件庫?如何保證元件庫的開發與使用的體驗一致性?

首先,我們需要一個 demo 專案,這個專案是 beeshell 元件庫的開發環境,是一個 React Native 應用。然後,我們把 beeshell 做為 demo 專案的依賴,在 demo 專案中下載安裝。現在,問題就變成了 node_modules 目錄中的 beeshell 如何和本地的 beeshell 原始碼進行同步。

npm link

我們知道,可以使用 npm link 來開發 npm 包,其工作原理如下圖:

在這裡插入圖片描述

本質是就是使用 Symbol link,但是我們建立好軟連結後,執行打包命令卻報錯了,錯誤資訊為 Expected path '/xxx/xxx/index.js' to be relative to one of project roots。

前端開發通常會用 Webpack 做為打包工具,而 React Native 應用使用的是 Metro,所以此處我們需要分析 Metro 來定位問題。

Metro

經過 Metro 的原始碼分析,我們發現 Metro 的打包方案與 Webpack 有較大差異,Webpack 是根據入口檔案,即配置中的 entry 屬性,遞迴解析依賴,構建依賴關係圖而 Metro 是爬取特定路徑下的所有檔案來構建依賴關係圖。分析發現 Metro 的特定路徑預設是執行打包命裡的路徑,以及 node_modules 下第一層目錄。

在這裡插入圖片描述

Metro 在爬取檔案的時候,通過軟連結找到了全域性的 beeshell 但是並沒有繼續判斷全域性的 beeshell 是否有軟連結,所以無法爬取 beeshell 原始碼部分。

軟連結

通過 ln -s 命令,直接建立 demo 專案 node_modules 下 beeshell 包 與 beeshell 原始碼的軟連結。

在這裡插入圖片描述
這種方式同時支援 Native 部分 iOS、Android 的原始碼開發,注意 Android 部分的需要在 setting.gradle 中呼叫 getCanonicalPath 方法獲取建立軟連結後的路徑。 通過試驗、發現問題、分析原始碼、定位問題、解決問題、方案完善這幾個步驟,完整的實現了 beeshell 元件庫的開發與使用的體驗一致性,同時提升了元件庫的開發效率。

本文為轉載文章,原文地址:tech.meituan.com/waimai-bees…

附: beeshell開源地址

相關文章