beeshell 是一個 React Native 應用的基礎組件庫,基於 0.53.3 版本,提供一整套開箱即用的高質量組件,包含 JavaScript(如下簡稱 JS)組件和複合組件(包含 Native 代碼),涉及前端(FE)、iOS、Android 三端技術,兼顧通用性和定製化,支持自定義主題,用於開發和服務企業級移動應用。如今已經在 GitHub 上開源,地址:github.com/meituan/bee…前端
截止目前,beeshell 中的組件已經在美團外賣移動端應用蜜蜂 App 中普遍應用,並且已經持續了一年多時間,經過了各類業務場景、操做系統、機型的實戰考驗,具有很好的穩定性、安全性和易用性,因此咱們將其開源,以期發揮出更大的應用價值。node
在開源以前,咱們對業界已經開源的組件庫進行了調研,這裏主要對比了 beeshell 與其餘組件庫的優點與劣勢,爲你們選擇組件庫提供參考意見。目前,業界開源的組件庫比較多,咱們在這裏僅選取 Github Star 數 5000 以上的組件庫,並從組件數量、通用性、定製化、是否包含原生功能、文檔完善程度五個維度來進行對比分析react
組件庫 | 組件數量 | 通用性 | 定製化 | 是否包含原生功能 | 文檔完善程度 |
---|---|---|---|---|---|
react-native-elements | 16 | 強,提供一套風格一致的 UI 控件 | 弱,若要定製化可能須要重寫 | 否 | 高 |
NativeBase | 28 | 強,提供一套風格一致的 UI 控件 | 中,支持主題變量 | 是 | 高 |
ant-design-mobile | 41 | 強,提供一套風格一致的 UI 控件 | 中,部分能夠支持定製化需求 | 是 | 低 |
beeshell | 25 | 強,提供一套風格一致的 UI 控件 | 強,不只支持主題變量,還支持使用繼承的方式進行定製化擴展 | 是 | 高 |
經過對比能夠看出,beeshell 只在組件數量上稍有劣勢,在其餘方面都一致或者優於其餘項目。由於 beeshell 具有了良好的系統架構,因此豐富組件數量只時間問題,並且咱們團隊也已經有了詳細的規劃來完善數量上的不足。git
系統設計是將一個實際問題轉換成相應解決方案的主動過程,是解決辦法的描述。在通用的軟件工程模型中,需求分析完成後的第一步就是系統設計。一個項目最終的穩定性、易用性在很大程度上也取決於系統設計這一步。github
beeshell 組件庫是爲了更加快速的搭建移動端應用,爲業務開發提供基礎技術支持,大幅提高開發人效。然而,面對不一樣的業務方、不一樣的功能需求、不一樣的 UI 規範與交互方式,如何有效的兼顧全部的需求?這對系統設計提出了更高的要求,下面以抽象層次逐層下降的方式來詳細介紹 beeshell 的系統設計。shell
這些年,React Native 的出現爲移動端開發提供了一種新的選擇。React Native 相比原生開發有着更高的開發效率,同時比 HTML五、Hybrid 的性能更好,因此可以脫穎而出,這也使得愈來愈多的開發者開始學習和使用 React Native。npm
beeshell 組件庫基於 React Native,向下經過 React Native 與 iOS、Android 平臺進行系統層面的交互,向上提供開發者友好的統一接口,抹平平臺差別,爲用戶開發業務功能提供服務支持。beeshell 扮演了一箇中間者的角色,從而保證了移動端應用基礎功能的穩定性、易用性。編程
框架設計肯定了 beeshell 的系統邊界,指明瞭包含的功能與不包含的功能之間的界限。明確了系統邊界,咱們才能繼續進行下面的分析、設計等工做。react-native
在進行組件庫的詳細設計以前,咱們提出了幾個設計原則:設計模式
總體上使用 JS 做爲統一入口,多層封裝隱藏實現細節,抹平 JS 與 Native、iOS 平臺與 Android 平臺的差別,開箱即用,下降了用戶的學習和使用成本。局部上基於 React Native 的技術特色,分紅 JS 組件部分和複合組件部分,兩部分推行「鬆耦合」的開發模式,使得 Native 部分擁有替換變動的能力,提高組件庫的靈活性。
複合組件部分能夠直接暴露 JS 接口,若是有須要,也能夠在 JS 組件部分進行定製化封裝。咱們儘可能保證 Native 部分功能的原子性、簡潔性,有任何定製化需求都使用 JS 來統一實現,遵循 JS 實現優先的設計原則,保證跨平臺通用的特性。下面分別介紹 JS 組件部分和複合組件部分的設計。
一個軟件的設計分爲三個設計層次:體系結構、代碼設計和可執行設計。咱們使用自上而下的方法,從體系結構開始進行 JS 組件部分的設計。
軟件的體系結構的風格一般有 7 種:管道和過濾器,面向對象,隱式請求,層次化,知識庫,解釋程序和過程控制。
JS 組件部分使用了層次化的體系結構風格,總體分紅三層:基礎工具、通用組件、擴展組件,從上到下通用性逐漸減弱、定製化逐漸加強,功能漸進式加強,經過分層設計,各層各司其職,兼顧通用性和定製化。
咱們擴展組件部分會提供大量的定製化組件,若是仍然不能知足需求,用戶就能夠借鑑擴展組件的實現,根據本身業務需求,在某一繼承層級上繼承通用組件,自行進行定製化擴展,這點充分體現了 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 組件,統一類名與調用方式,保證了跨平臺通用性。
iOS 平臺的 Datepicker 組件:
Android 平臺的 Datepicker 組件:
Datepicker 是使用 JS 徹底實現了一個完整功能,可是有的狀況不須要實現完整的功能,咱們能夠經過 React Native 提供的 Platform
來進行局部的跨平臺處理,例如 TextInput 組件。
iOS 平臺的 TextInput 組件:
Android 平臺的 TextInput 組件:
咱們能夠看到,在 Andriod 平臺並無清空圖標,爲了抹平平臺的差別,提供更好的通用性,咱們開發了 Input 組件,對 TextInput 進行封裝與優化,利用 Platform
定位 Android 平臺提供清空功能,
Input 組件在 Android 平臺的效果:
總之,beeshell 對跨平臺通用性作了進一步的優化,遵循 JS 實現優先的原則,配合 Platform
平臺定位 API 爲組件的易用性、通用性提供了更好的保障。
隨着移動互聯網的快速發展,各種移動端產品涌現而且不斷髮展,這也讓軟件知識不斷被普及,業務方對產品功能的定位逐漸從廠商主導轉變爲用戶主導。產品功能更加精準,個性化、細化、深化是必然趨勢,經過定製化服務來知足產品發展的要求也應運而生。不一樣行業、不一樣類型的產品,功能、特色各不相同,用某一種既定的軟件產品來知足不一樣類型的需求,其適用性可想而知。定製化有良好的技術架構和技術優點,可定製、可擴展、可集成、跨平臺,在個性化需求的處理方面,有着很好的優點,因此咱們須要定製化。
綜上所述,beeshell 把定製化做爲核心特性,力求知足不一樣產品的定製化需求,下文將從組件的樣式定製化和功能定製化兩方面來進行闡述。
beeshell 的設計規範支持必定程度的樣式定製,以知足業務和品牌上多樣化的視覺需求,包括但不限於品牌色、圓角、邊框等的視覺定製。
在組件庫設計之初,就已經統一好了 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 系列,使用繼承的方式實現。有人可能會問爲何使用繼承而不用使用組合?前文已經講過,組合的主要目的是代碼複用,而繼承的主要目的是擴展。考慮到彈窗交互有不少定製化的可能性,爲了知足更好的擴展性,咱們選擇了繼承。
首先咱們看下幾個組件的實現效果圖,對 Modal 系列先有一個直觀的認識。
Modal 組件:
提供了遮罩、彈出容器以及淡入淡出(Fade)動畫效果,彈出內容部分徹底由用戶自定義。這個組件通用性極強,沒有任何定製化的功能。這裏須要說明下,動畫部分獨立實現,提供了 FadeAnimated 和 SlideAnimated 兩個子類,使用了策略模式與 Modal 系列集成,Modal 組件默認集成 FadeAnimated。
ConfirmModal 組件:
繼承 Modal 組件,對彈出內容作了必定程度的定製化擴展,支持標題、確認按鈕、取消按鈕以及自定義 body 部分的功能,通用性減弱,定製化加強。
SlideModal 組件:
繼承 Modal 組件,對動畫、彈出容器作了重寫,在初始化時實例化 SlideAnimated 類型對象,完成上拉、下拉動畫,同時支持了自定義彈出位置的功能。
PageModal 組件:
繼承 SlideModal 組件,對彈出內容作了定製化擴展,支持標題、確認按鈕、取消按鈕以及自定義 body 功能,通用性減弱,定製化加強。
CheckboxModal 組件:
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 組件:
咱們須要精確的計算容器以及每一項元素的高度,才能正確獲得當前選中的項在數據模型(數組)中的索引。如今面臨的問題是:在組件渲染完成後的生命週期 componentDidMount
並不能拿到正確容器的高度爲,而使用 setTimeout
也會有延遲時長設置爲多少的問題。咱們選擇使用遞歸來解決,一次 setTimeout
不行就執行屢次。
這裏使用了交互遞歸,反覆執行,直到獲得有效的元素尺寸。
React Native 爲用戶提供了 style 屬性來控制元素的樣式,咱們能夠手動設置相關 UI 元素的尺寸。可是,在一些 Android 機器上,咱們設置的元素尺寸與 measure
方法獲取的尺寸信息不一致,通過大量 Android 機器的實際的測試,咱們獲得的結論是:有零點幾像素的偏差。
咱們把經過 measure
方法獲得尺寸信息進行向上與向下取整,獲得一個閾值範圍,手動設置的尺寸信息只要在這個閾值範圍內,就認爲是有效尺寸,這種容錯機制有效的兼容了極端狀況,提升了組件的穩定性。
在使用 Form 組件時,最多見的需求就是校驗功能,一般組件庫的 Form 組件都會內置校驗功能。然而,由於校驗方式有同步與異步兩種,校驗結果展現的樣式、位置五花八門,這就致使了校驗功能的複雜度變得很高。
絕對定位:
Static 定位:
自定義位置
如何有效的兼顧不一樣的需求?咱們提出了校驗獨立實現的方式,在使用 Form 組件的父組件中,使用 CVD 來定義、配置校驗規則,校驗結果輸出到統一的數據結構(單一數據源),基於這個數據結構,咱們就能在任意時機、任意位置、使用任意樣式來展現校驗信息。
下面咱們先介紹下 CVD:
CVD 是一個針對複雜表單錄入場景的分層解決方案,輕量級、跨平臺、易擴展,內置在 beeshell 組件庫中,能夠直接使用。
CVD 把表單某個控件的錄入的流程分紅三層:
每一層都對單一數據源 Store 進行不可變數據更新,符合交互內聚和順序內聚,內聚程度高。
每一層使用函數式組合的方式,定義 key(表單控件的惟一標誌)與 key 對應的回調函數,避免了批量 if else
,能夠有效下降程序的圓環複雜度。
下面以 Input 組件錄入姓名爲例,來具體說明,代碼以下:
在 onChange
中獲取用戶輸入,調用 cvd.flow
而後就能夠經過 cvd.getStore
獲取到結果:
經過校驗功能獨立實現,把校驗信息輸出到 Store 中,在須要的時候從 Store 中獲取校驗信息,能夠更加精細化的控制元素的樣式、位置與佈局,兼容各類定製化需求。不少時候,只有咱們想不到,沒有作不到。
代碼的終極目標有兩個,第一個是實現需求,第二個是提升代碼質量和可維護性。測試是爲了提升代碼質量和可維護性,是實現代碼的第二個目標的一種方法。
單元測試(Unit Testing),是指對軟件中的最小可測試單元進行檢查和驗證。在結構化編程的時代,單元測試中單元指的就是函數。beeshell 組件庫全面使用單元測試,由組件的開發者完成。研究成果代表,不管何時做出修改都須要進行完整的迴歸測試,對於提供基礎功能的組件來講更是如此,在生命週期中儘早地對軟件產品進行測試將使效率和質量都獲得最好的保證。Bug 發現的越晚,修改它所需的成本就越高,單元測試是一個在早期抓住 Bug 的機會。
單元測試的優勢有如下幾點:
beeshell 組件庫使用 Jest 作爲單元測試的工具,自帶斷言、測試覆蓋率工具,實現開箱即用。
測試用例的核心是輸入數據,咱們會選擇具備表明性的數據做爲輸入數據,主要有三種:正常輸入,邊界輸入,非法輸入,下面以組件庫中提供的 isLeapYear
工具函數來舉例說明,代碼以下:
Jest 使用 test
函數來描述一個測試用例,其中的 toBe
邊是一句斷言。
函數使用了外部數據,正常輸入確定會有,這裏的 2000
和 '2000'
都是正常輸入;邊界輸入和非法輸入並非全部的函數都有,這裏爲了說明使用了有這兩種輸入的例子,邊界輸入是有效輸入的極限值,這裏 0
和 Infinity
是邊界輸入;非法輸入是正常取值範圍之外的數據, 'xx'
和 false
則是非法輸入。通常狀況下,考慮以上三種輸入能夠找出函數的基本功能點,單元測試與代碼編寫是「一體兩面」的關係,編碼時對上述三種輸入都是應該考慮的,不然代碼的健壯性就會出現問題。
上文所說的測試是針對程序的功能來設計的,就是所謂的「黑盒測試」。單元測試還須要從另外一個角度來設計測試數據,即針對程序的邏輯結構來設計測試用例,就是所謂的「白盒測試」。
仍是以 isLeapYear
函數來進行說明,其代碼以下:
這裏有一個 if else
語句,若是咱們只提供一個 2000
的輸入,只會測試到 if
語句,而不會測試 else
語句。雖然,在黑盒測試足夠充分的狀況下,白盒測試沒有必要,惋惜「足夠充分」只是一種理想狀態,難於衡量測試的完整性是黑盒測試的主要缺陷。而白盒測試偏偏具備易於衡量測試完整性的優勢,二者之間具備極好的互補性,例如:完成功能測試後統計語句覆蓋率,若是語句覆蓋未完成,極可能是未覆蓋的語句所對應的功能點未測試。
白盒測試也是比較常見的需求,Jest 內置了測試覆蓋率工具,能夠直接在命令中添加 --coverage
參數即可以輸出單元測試覆蓋率的報告,結果以下:
能夠看到代碼的每一行都覆蓋到了 Coverage 爲 100%,在很大程度上保證了功能的穩定性。
想要確保組件庫的 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 保證代碼質量。
beeshell 組件庫的分析結果概況如圖:
可靠性達到 A 級別,是最高等級,表示無 Bug:
安全性達到 A 級別,是最高等級,表示無漏洞:
測試覆蓋率平均達到 70% 以上
beeshell 組件庫使用 npm 包的形式下載使用,下載成功後會放置在項目根目錄的 node_modules 目錄,而後在項目中經過引入模塊的方式,引入 beeshell 的組件來使用。
那咱們如何開發組件庫?如何保證組件庫的開發與使用的體驗一致性?
首先,咱們須要一個 demo 項目,這個項目是 beeshell 組件庫的開發環境,是一個 React Native 應用。而後,咱們把 beeshell 作爲 demo 項目的依賴,在 demo 項目中下載安裝。
如今,咱們的問題就變成了 node_modules 目錄中的 beeshell 如何和本地的 beeshell 源碼進行同步。
咱們知道可使用 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 的打包方案與 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 組件庫的開發與使用的體驗一致性,同時提高了組件庫的開發效率。
咱們的目標是把 beeshell 建設成爲一個大而全的組件庫,不只會不斷豐富 JS 組件,並且會不斷增強複合組件去支持更多的底層功能。由於咱們支持所有引入和按需引入兩種方式,用戶不須要擔憂會引入過多無用組件而使得包體積過大,影響開發和使用效率。
beeshell 目前提供了 20+ 組件以及基礎工具,基於良好的架構設計、開發體驗,爲咱們不斷地豐富組件庫提供了良好的基礎。同時在開發 React Native 應用的幾年時間中,咱們已經積累了 50+ 基礎以及業務組件,咱們後續會把積累的組件進行梳理與調整,所有遷移到 beeshell 中。由於咱們的組件主要來源於咱們的業務需求,可是業務場景有限,可能會使得 beeshell 的發展受到限制,因此咱們將其開源。但願藉助社區的力量不斷豐富組件庫的功能,盡最大努力覆蓋到移動應用方方面面的功能,歡迎你們獻計獻策,多多支持。
咱們爲組件庫發展規劃了三個階段: