beeshellReact Native 應(yīng)用基礎(chǔ)組件庫
介紹
beeshell 是一個(gè) React Native 應(yīng)用的基礎(chǔ)組件庫,基于 0.53.3 版本,提供一整套開箱即用的高質(zhì)量組件,包含 JavaScript(以下簡稱 JS)組件和復(fù)合組件(包含 Native 代碼),涉及前端(FE)、iOS、Android 三端技術(shù),兼顧通用性和定制化,支持自定義主題,用于開發(fā)和服務(wù)企業(yè)級(jí)移動(dòng)應(yīng)用。
截止目前,beeshell 中的組件已經(jīng)在美團(tuán)外賣移動(dòng)端應(yīng)用蜜蜂 App 中廣泛應(yīng)用,而且已經(jīng)持續(xù)了一年多時(shí)間,通過了各種業(yè)務(wù)場景、操作系統(tǒng)、機(jī)型的實(shí)戰(zhàn)考驗(yàn),具備很好的穩(wěn)定性、安全性和易用性,所以我們將其開源,以期發(fā)揮出更大的應(yīng)用價(jià)值。
特性
UI 樣式的一致性和定制化。
通用性。主要使用 JS 來實(shí)現(xiàn),保證跨平臺(tái)通用性。
定制化。我們在比較細(xì)的粒度上對組件進(jìn)行拆分,通過繼承的方式層層依賴,功能漸進(jìn)式增強(qiáng),為在任意層級(jí)上的繼承擴(kuò)展、個(gè)性化定制提供了可能。
原生功能支持。組件庫中的復(fù)合組件包含 Native 代碼,支持圖片選擇、定位等原生功能。
功能豐富。不僅僅提供組件,還提供了基礎(chǔ)工具、動(dòng)畫以及 UI 規(guī)范。
完善的文檔和使用示例。
對比
在開源之前,我們對業(yè)界已經(jīng)開源的組件庫進(jìn)行了調(diào)研,這里主要對比了 beeshell 與其他組件庫的優(yōu)勢與劣勢,為大家選擇組件庫提供參考意見。目前,業(yè)界開源的組件庫比較多,我們在這里僅選取 Github Star 數(shù) 5000 以上的組件庫,并從組件數(shù)量、通用性、定制化、是否包含原生功能、文檔完善程度五個(gè)維度來進(jìn)行對比分析
| 組件庫 | 組件數(shù)量 | 通用性 | 定制化 | 是否包含原生功能 | 文檔完善程度 |
|---|---|---|---|---|---|
| react-native-elements | 16 | 強(qiáng),提供一套風(fēng)格一致的 UI 控件 | 弱,若要定制化可能需要重寫 | 否 | 高 |
| NativeBase | 28 | 強(qiáng),提供一套風(fēng)格一致的 UI 控件 | 中,支持主題變量 | 是 | 高 |
| ant-design-mobile | 41 | 強(qiáng),提供一套風(fēng)格一致的 UI 控件 | 中,部分可以支持定制化需求 | 是 | 低 |
| beeshell | 25 | 強(qiáng),提供一套風(fēng)格一致的 UI 控件 | 強(qiáng),不僅支持主題變量,還支持使用繼承的方式進(jìn)行定制化擴(kuò)展 | 是 | 高 |
通過對比可以看出,beeshell 只在組件數(shù)量上稍有劣勢,在其他方面都一致或者優(yōu)于其他項(xiàng)目。因?yàn)?beeshell 具備了良好的系統(tǒng)架構(gòu),所以豐富組件數(shù)量只時(shí)間問題,而且我們團(tuán)隊(duì)也已經(jīng)有了詳細(xì)的規(guī)劃來完善數(shù)量上的不足。
系統(tǒng)設(shè)計(jì)
系統(tǒng)設(shè)計(jì)是將一個(gè)實(shí)際問題轉(zhuǎn)換成相應(yīng)解決方案的主動(dòng)過程,是解決辦法的描述。在通用的軟件工程模型中,需求分析完成后的第一步就是系統(tǒng)設(shè)計(jì)。一個(gè)項(xiàng)目最終的穩(wěn)定性、易用性在很大程度上也取決于系統(tǒng)設(shè)計(jì)這一步。
beeshell 組件庫是為了更加快速的搭建移動(dòng)端應(yīng)用,為業(yè)務(wù)開發(fā)提供基礎(chǔ)技術(shù)支持,大幅提升開發(fā)人效。然而,面對不同的業(yè)務(wù)方、不同的功能需求、不同的 UI 規(guī)范與交互方式,如何有效的兼顧所有的需求?這對系統(tǒng)設(shè)計(jì)提出了更高的要求,下面以抽象層次逐層降低的方式來詳細(xì)介紹 beeshell 的系統(tǒng)設(shè)計(jì)。
框架設(shè)計(jì)
這些年,React Native 的出現(xiàn)為移動(dòng)端開發(fā)提供了一種新的選擇。React Native 相比原生開發(fā)有著更高的開發(fā)效率,同時(shí)比 HTML5、Hybrid 的性能更好,所以能夠脫穎而出,這也使得越來越多的開發(fā)者開始學(xué)習(xí)和使用 React Native。
beeshell 組件庫基于 React Native,向下通過 React Native 與 iOS、Android 平臺(tái)進(jìn)行系統(tǒng)層面的交互,向上提供開發(fā)者友好的統(tǒng)一接口,抹平平臺(tái)差異,為用戶開發(fā)業(yè)務(wù)功能提供服務(wù)支持。beeshell 扮演了一個(gè)中間者的角色,從而保證了移動(dòng)端應(yīng)用基礎(chǔ)功能的穩(wěn)定性、易用性。
框架設(shè)計(jì)確定了 beeshell 的系統(tǒng)邊界,指明了包含的功能與不包含的功能之間的界限。明確了系統(tǒng)邊界,我們才能繼續(xù)進(jìn)行下面的分析、設(shè)計(jì)等工作。
設(shè)計(jì)原則
在進(jìn)行組件庫的詳細(xì)設(shè)計(jì)之前,我們提出了幾個(gè)設(shè)計(jì)原則:
JS 實(shí)現(xiàn)優(yōu)先。使用 JS 來實(shí)現(xiàn)功能有幾個(gè)好處:跨平臺(tái)通用性、更高的開發(fā)效率、更低的學(xué)習(xí)和使用成本。
繼承與組合靈活運(yùn)用。繼承和組合都是實(shí)現(xiàn)功能復(fù)用、代碼復(fù)用的有效的設(shè)計(jì)技巧,都是設(shè)計(jì)模式中的基礎(chǔ)結(jié)構(gòu)。繼承允許子類覆蓋重寫父類的實(shí)現(xiàn)細(xì)節(jié),父類的實(shí)現(xiàn)對于子類是可見的,一般稱之為“白盒復(fù)用”,這對組件的定制化擴(kuò)展很有效,beeshell 強(qiáng)大的定制化擴(kuò)展的能力就是基于繼承實(shí)現(xiàn);組合是 React 推薦的方式,React 組件具有強(qiáng)大的組合模型,整體類和部分類之間不會(huì)去關(guān)心各自的實(shí)現(xiàn)細(xì)節(jié),它們之間的實(shí)現(xiàn)細(xì)節(jié)是不可見的,一般稱之為“黑盒復(fù)用”。beeshell 也廣泛使用了組合,基于通用型的組件組合出更加豐富、強(qiáng)大、個(gè)性化的功能,在一定程度上提高了 beeshell 的定制化的能力。
低耦合、高內(nèi)聚。一個(gè) beeshell 組件本質(zhì)上就是一個(gè) React 組件,React 組件之間主要通過 Props 通信,這屬于數(shù)據(jù)耦合,相比于內(nèi)容耦合、控制耦合等其他耦合方式,數(shù)據(jù)耦合是耦合程度最低的一種,受益于 React 的實(shí)現(xiàn),beeshell 組件低耦合是自然而然的;而要做的高內(nèi)聚,則對組件的編碼實(shí)現(xiàn)方式有一定的要求,我們推行內(nèi)聚方式中內(nèi)聚程度比較高的交互內(nèi)聚和順序內(nèi)聚。使用單一數(shù)據(jù)源,使各個(gè)元素操作相同的數(shù)據(jù)結(jié)構(gòu),實(shí)現(xiàn)交互內(nèi)聚。使用不可變數(shù)據(jù)更新的方式,上一個(gè)環(huán)節(jié)的輸出是下一個(gè)環(huán)節(jié)的輸入,像流水線一樣處理邏輯,這便是順序內(nèi)聚。
方案設(shè)計(jì)
整體上使用 JS 作為統(tǒng)一入口,多層封裝隱藏實(shí)現(xiàn)細(xì)節(jié),抹平 JS 與 Native、iOS 平臺(tái)與 Android 平臺(tái)的差異,開箱即用,降低了用戶的學(xué)習(xí)和使用成本。局部上基于 React Native 的技術(shù)特點(diǎn),分成 JS 組件部分和復(fù)合組件部分,兩部分推行“松耦合”的開發(fā)模式,使得 Native 部分擁有替換變更的能力,提升組件庫的靈活性。
復(fù)合組件部分可以直接暴露 JS 接口,如果有需要,也可以在 JS 組件部分進(jìn)行定制化封裝。我們盡量保證 Native 部分功能的原子性、簡潔性,有任何定制化需求都使用 JS 來統(tǒng)一實(shí)現(xiàn),遵循 JS 實(shí)現(xiàn)優(yōu)先的設(shè)計(jì)原則,保證跨平臺(tái)通用的特性。下面分別介紹 JS 組件部分和復(fù)合組件部分的設(shè)計(jì)。
JS 組件部分設(shè)計(jì)
一個(gè)軟件的設(shè)計(jì)分為三個(gè)設(shè)計(jì)層次:體系結(jié)構(gòu)、代碼設(shè)計(jì)和可執(zhí)行設(shè)計(jì)。我們使用自上而下的方法,從體系結(jié)構(gòu)開始進(jìn)行 JS 組件部分的設(shè)計(jì)。
軟件的體系結(jié)構(gòu)的風(fēng)格通常有 7 種:管道和過濾器,面向?qū)ο螅[式請求,層次化,知識(shí)庫,解釋程序和過程控制。
JS 組件部分使用了層次化的體系結(jié)構(gòu)風(fēng)格,整體分成三層:基礎(chǔ)工具、通用組件、擴(kuò)展組件,從上到下通用性逐漸減弱、定制化逐漸增強(qiáng),功能漸進(jìn)式增強(qiáng),通過分層設(shè)計(jì),各層各司其職,兼顧通用性和定制化。
基礎(chǔ)工具(common):最基礎(chǔ)的、通用的部分,包含 JS Utils、動(dòng)畫定義、UI 規(guī)范等。
通用組件(components):把功能相似的組件進(jìn)行歸類,整理成一個(gè)個(gè)系列,每個(gè)系列內(nèi)部使用繼承的方式實(shí)現(xiàn),層層依賴,功能漸進(jìn)式增強(qiáng),該部分專注通用性,不考慮定制化需求,保證代碼的簡潔性。同時(shí),在比較細(xì)的粒度對組件進(jìn)行拆分,提供了良好的可擴(kuò)展性。
擴(kuò)展組件(modules):是對通用組件的繼承擴(kuò)展、組合應(yīng)用,該部分專注定制化,在最大程度上滿足業(yè)務(wù)上的需求,通用性較低。
我們擴(kuò)展組件部分會(huì)提供大量的定制化組件,如果仍然不能滿足需求,用戶就可以借鑒擴(kuò)展組件的實(shí)現(xiàn),根據(jù)自己業(yè)務(wù)需求,在某一繼承層級(jí)上繼承通用組件,自行進(jìn)行定制化擴(kuò)展,這點(diǎn)充分體現(xiàn)了 beeshell 定制化的能力。
復(fù)合組件部分設(shè)計(jì)
既然是 React Native 組件庫當(dāng)然少不了 Native 部分,復(fù)合組件包含 Native 的功能。beeshell 組件庫已經(jīng)完成了 Native 部分的集成方案與規(guī)范,有良好的開發(fā)與使用體驗(yàn),可以不斷的集成原生功能。
復(fù)合組件部分通過 JS 封裝接口,保證了跨平臺(tái)。Native 部分主要分成 Native Bridge 和純 Native 兩大部分,Bridge 是針對 React Native 的封裝,必須在組件庫中實(shí)現(xiàn);而純 Native 部分則可以通過 Pods/Gradle 依賴三方實(shí)現(xiàn),有效的吸收利用原生開發(fā)的技術(shù)積累。
組件庫實(shí)現(xiàn)
跨平臺(tái)通用性保障
React Native 提供了一些內(nèi)置組件,我們能使用 JS 來實(shí)現(xiàn)功能都是基于這些內(nèi)置組件,這些內(nèi)置的組件一些是跨平臺(tái)通用的組件,如:View、Text、TextInput;而另一些是兩個(gè)平臺(tái)分別實(shí)現(xiàn)的,如 DatePickerIOS 和 DatePickerAndroid、AlertIOS 和 ToastAndroid。跨平臺(tái)組件當(dāng)然沒有什么問題,我們可以專注業(yè)務(wù)功能的開發(fā),問題是這些非跨平臺(tái)的組件,給我們的業(yè)務(wù)功能開發(fā)帶來極大困擾,下面舉例說明。
iOS 平臺(tái)的 DatePickerIOS 組件:
Android 平臺(tái)的 DatePickerAndroid 組件:
不僅功能交互完全不同,而且類名、調(diào)用方式各異,這不僅滿足不了業(yè)務(wù)需求,而且也有很高的學(xué)習(xí)和使用成本。這樣類似的組件還有很多,如何抹平平臺(tái)的差異,實(shí)現(xiàn)跨平臺(tái)?我們提出的方案是優(yōu)先使用 JS 來實(shí)現(xiàn)功能,這也是我們組件庫的設(shè)計(jì)原則。
針對上面的問題我們開發(fā)了基于 ScrollView 的 Datepicker 組件,統(tǒng)一類名與調(diào)用方式,保證了跨平臺(tái)通用性。
iOS 平臺(tái)的 Datepicker 組件:
Android 平臺(tái)的 Datepicker 組件:
Datepicker 是使用 JS 完全實(shí)現(xiàn)了一個(gè)完整功能,但是有的情況不需要實(shí)現(xiàn)完整的功能,我們可以通過 React Native 提供的 Platform 來進(jìn)行局部的跨平臺(tái)處理,例如 TextInput 組件。
iOS 平臺(tái)的 TextInput 組件:
Android 平臺(tái)的 TextInput 組件:
我們可以看到,在 Andriod 平臺(tái)并沒有清空圖標(biāo),為了抹平平臺(tái)的差異,提供更好的通用性,我們開發(fā)了 Input 組件,對 TextInput 進(jìn)行封裝與優(yōu)化,利用 Platform 定位 Android 平臺(tái)提供清空功能,
Input 組件在 Android 平臺(tái)的效果:
總之,beeshell 對跨平臺(tái)通用性做了進(jìn)一步的優(yōu)化,遵循 JS 實(shí)現(xiàn)優(yōu)先的原則,配合 Platform 平臺(tái)定位 API 為組件的易用性、通用性提供了更好的保障。
定制化支持
隨著移動(dòng)互聯(lián)網(wǎng)的快速發(fā)展,各類移動(dòng)端產(chǎn)品涌現(xiàn)并且不斷發(fā)展,這也讓軟件知識(shí)不斷被普及,業(yè)務(wù)方對產(chǎn)品功能的定位逐漸從廠商主導(dǎo)轉(zhuǎn)變?yōu)橛脩糁鲗?dǎo)。產(chǎn)品功能更加精準(zhǔn),個(gè)性化、細(xì)化、深化是必然趨勢,通過定制化服務(wù)來滿足產(chǎn)品發(fā)展的要求也應(yīng)運(yùn)而生。不同行業(yè)、不同類型的產(chǎn)品,功能、特點(diǎn)各不相同,用某一種既定的軟件產(chǎn)品來滿足不同類型的需求,其適用性可想而知。定制化有良好的技術(shù)架構(gòu)和技術(shù)優(yōu)勢,可定制、可擴(kuò)展、可集成、跨平臺(tái),在個(gè)性化需求的處理方面,有著很好的優(yōu)勢,所以我們需要定制化。
綜上所述,beeshell 把定制化作為核心特性,力求滿足不同產(chǎn)品的定制化需求,下文將從組件的樣式定制化和功能定制化兩方面來進(jìn)行闡述。
樣式定制化
beeshell 的設(shè)計(jì)規(guī)范支持一定程度的樣式定制,以滿足業(yè)務(wù)和品牌上多樣化的視覺需求,包括但不限于品牌色、圓角、邊框等的視覺定制。
在組件庫設(shè)計(jì)之初,就已經(jīng)統(tǒng)一好了 UI 規(guī)范。我們根據(jù) UI 規(guī)范,統(tǒng)一定義樣式變量并放置在基礎(chǔ)工具層中,即 beeshell/common/styles/varibles.js 文件中,在 React Native 應(yīng)用中,樣式變量其實(shí)就是普通的 JS 變量,可以很方便的進(jìn)行復(fù)用與重寫操作。React Native 提供了 StyleSheet 通過創(chuàng)建一個(gè)樣式表,使用 ID 來引用樣式,減少頻繁創(chuàng)建新的樣式對象,在組件庫的樣式變量應(yīng)用中靈活使用 StyleSheet.create和 StyleSheet.flatten 來獲取樣式 ID 和樣式對象。
在每個(gè)組的實(shí)現(xiàn)中,會(huì)事先引入基礎(chǔ)工具層中的樣式變量,使用統(tǒng)一的變量對象而不是在組件中自行定義,這樣就保證了 UI 樣式的一致性。同時(shí),beeshell 提供了重置樣式變量的 API,可以實(shí)現(xiàn)一鍵換膚。我們推薦 beeshell 的用戶在開發(fā)移動(dòng)應(yīng)用時(shí),事先定義好樣式變量。一方面使用自己的樣式變量重置 beeshell 的樣式變量;另一方面在業(yè)務(wù)功能開發(fā)時(shí),使用自己定義好的樣式變量,從而保證整體 UI 的一致性。
功能定制化
樣式定制化可以從宏觀和整體的角度來實(shí)現(xiàn),而功能的定制化則需要具體問題具體分析,從微觀和局部的角度來分析和實(shí)現(xiàn)。下文將以 Modal 系列的實(shí)現(xiàn)為例,來詳細(xì)介紹功能定制化。
在移動(dòng)端的彈窗交互,與 PC 端相比一般會(huì)比較簡單,我們把模態(tài)框、下拉菜單、信息提示等交互類似的組件統(tǒng)一歸類為 Modal 系列,使用繼承的方式實(shí)現(xiàn)。有人可能會(huì)問為什么使用繼承而不用使用組合?前文已經(jīng)講過,組合的主要目的是代碼復(fù)用,而繼承的主要目的是擴(kuò)展。考慮到彈窗交互有很多定制化的可能性,為了滿足更好的擴(kuò)展性,我們選擇了繼承。
首先我們看下幾個(gè)組件的實(shí)現(xiàn)效果圖,對 Modal 系列先有一個(gè)直觀的認(rèn)識(shí)。
Modal 組件:
提供了遮罩、彈出容器以及淡入淡出(Fade)動(dòng)畫效果,彈出內(nèi)容部分完全由用戶自定義。這個(gè)組件通用性極強(qiáng),沒有任何定制化的功能。這里需要說明下,動(dòng)畫部分獨(dú)立實(shí)現(xiàn),提供了 FadeAnimated 和 SlideAnimated 兩個(gè)子類,使用了策略模式與 Modal 系列集成,Modal 組件默認(rèn)集成 FadeAnimated。
ConfirmModal 組件:
繼承 Modal 組件,對彈出內(nèi)容做了一定程度的定制化擴(kuò)展,支持標(biāo)題、確認(rèn)按鈕、取消按鈕以及自定義 body 部分的功能,通用性減弱,定制化增強(qiáng)。
SlideModal 組件:
繼承 Modal 組件,對動(dòng)畫、彈出容器做了重寫,在初始化時(shí)實(shí)例化 SlideAnimated 類型對象,完成上拉、下拉動(dòng)畫,同時(shí)支持了自定義彈出位置的功能。
PageModal 組件:
繼承 SlideModal 組件,對彈出內(nèi)容做了定制化擴(kuò)展,支持標(biāo)題、確認(rèn)按鈕、取消按鈕以及自定義 body 功能,通用性減弱,定制化增強(qiáng)。
CheckboxModal 組件:
CheckboxModal 組件由 PageModal 和 Checkbox 兩個(gè)組件使用組合的方式實(shí)現(xiàn),基于通用型組件組合出了更加強(qiáng)大功能,遵循繼承與組合靈活運(yùn)用的設(shè)計(jì)原則。
通過以上部分,我們已經(jīng)對 Modal 系列已經(jīng)有了直觀的認(rèn)識(shí),然后我們來看下 Modal 系列的類圖以及分層:
動(dòng)畫部分在基礎(chǔ)工具(common)中實(shí)現(xiàn);在通用組件(components)中 Modal 組件聚合 FadeAnimated 動(dòng)畫,同時(shí)因?yàn)?SlideModal、ConfirmModal 比較通用,也在該部分實(shí)現(xiàn);CheckboxModal 則定制化比較強(qiáng),歸類到擴(kuò)展組件(modules)中。通過這種方式的分層,三層各司其職,使得組件庫的層次結(jié)構(gòu)更加清晰,不僅實(shí)現(xiàn)了定制化,還保證了通用部分的簡潔性和可維護(hù)性。
復(fù)雜 Case 處理
相互遞歸處理異步渲染
React Native 應(yīng)用的 JS 線程和 UI 線程是兩個(gè)線程,與瀏覽器中共用一個(gè)線程的實(shí)現(xiàn)不同,所以我們可以看到 React Native 提供的操作 UI 元素的 API,都是通過回調(diào)函數(shù)的方式進(jìn)行調(diào)用。
受益于 React,我們一般不需要直接操作 UI 元素,但是有的組件確實(shí)需要復(fù)雜的 UI 操作,例如完全由 JS 實(shí)現(xiàn)的 Scrollerpicker 組件:
我們需要精確的計(jì)算容器以及每一項(xiàng)元素的高度,才能正確得到當(dāng)前選中的項(xiàng)在數(shù)據(jù)模型(數(shù)組)中的索引。現(xiàn)在面臨的問題是:在組件渲染完成后的生命周期 componentDidMount 并不能拿到正確容器的高度為,而使用 setTimeout 也會(huì)有延遲時(shí)長設(shè)置為多少的問題。我們選擇使用遞歸來解決,一次 setTimeout不行就執(zhí)行多次。
這里使用了交互遞歸,反復(fù)執(zhí)行,直到得到有效的元素尺寸。
UI 尺寸容錯(cuò)機(jī)制
React Native 為用戶提供了 style 屬性來控制元素的樣式,我們可以手動(dòng)設(shè)置相關(guān) UI 元素的尺寸。但是,在一些 Android 機(jī)器上,我們設(shè)置的元素尺寸與 measure 方法獲取的尺寸信息不一致,經(jīng)過大量 Android 機(jī)器的實(shí)際的測試,我們得到的結(jié)論是:有零點(diǎn)幾像素的誤差。
我們把通過 measure 方法得到尺寸信息進(jìn)行向上與向下取整,得到一個(gè)閾值范圍,手動(dòng)設(shè)置的尺寸信息只要在這個(gè)閾值范圍內(nèi),就認(rèn)為是有效尺寸,這種容錯(cuò)機(jī)制有效的兼容了極端情況,提高了組件的穩(wěn)定性。
精細(xì)化布局控制
在使用 Form 組件時(shí),最常見的需求就是校驗(yàn)功能,通常組件庫的 Form 組件都會(huì)內(nèi)置校驗(yàn)功能。然而,因?yàn)樾r?yàn)方式有同步與異步兩種,校驗(yàn)結(jié)果展示的樣式、位置五花八門,這就導(dǎo)致了校驗(yàn)功能的復(fù)雜度變得很高。
絕對定位:
Static 定位:
自定義位置
如何有效的兼顧不同的需求?我們提出了校驗(yàn)獨(dú)立實(shí)現(xiàn)的方式,在使用 Form 組件的父組件中,使用 CVD 來定義、配置校驗(yàn)規(guī)則,校驗(yàn)結(jié)果輸出到統(tǒng)一的數(shù)據(jù)結(jié)構(gòu)(單一數(shù)據(jù)源),基于這個(gè)數(shù)據(jù)結(jié)構(gòu),我們就能在任意時(shí)機(jī)、任意位置、使用任意樣式來展示校驗(yàn)信息。
下面我們先介紹下 CVD:
CVD 是一個(gè)針對復(fù)雜表單錄入場景的分層解決方案,輕量級(jí)、跨平臺(tái)、易擴(kuò)展,內(nèi)置在 beeshell 組件庫中,可以直接使用。
CVD 把表單某個(gè)控件的錄入的流程分成三層:
Connector 連接器,把用戶輸入的信息轉(zhuǎn)化成所需的數(shù)據(jù)格式。
Validator 校驗(yàn)器,對格式化的數(shù)據(jù)進(jìn)行校驗(yàn)。
Dependency 依賴處理器,處理當(dāng)前控件與其他控件的依賴關(guān)系。
每一層都對單一數(shù)據(jù)源 Store 進(jìn)行不可變數(shù)據(jù)更新,符合交互內(nèi)聚和順序內(nèi)聚,內(nèi)聚程度高。
每一層使用函數(shù)式組合的方式,定義 key(表單控件的唯一標(biāo)志)與 key 對應(yīng)的回調(diào)函數(shù),避免了批量 if else,可以有效降低程序的圓環(huán)復(fù)雜度。
下面以 Input 組件錄入姓名為例,來具體說明,代碼如下:
在 onChange 中獲取用戶輸入,調(diào)用 cvd.flow 然后就可以通過 cvd.getStore 獲取到結(jié)果:
通過校驗(yàn)功能獨(dú)立實(shí)現(xiàn),把校驗(yàn)信息輸出到 Store 中,在需要的時(shí)候從 Store 中獲取校驗(yàn)信息,可以更加精細(xì)化的控制元素的樣式、位置與布局,兼容各種定制化需求。很多時(shí)候,只有我們想不到,沒有做不到。
測試
代碼的終極目標(biāo)有兩個(gè),第一個(gè)是實(shí)現(xiàn)需求,第二個(gè)是提高代碼質(zhì)量和可維護(hù)性。測試是為了提高代碼質(zhì)量和可維護(hù)性,是實(shí)現(xiàn)代碼的第二個(gè)目標(biāo)的一種方法。
單元測試
單元測試(Unit Testing),是指對軟件中的最小可測試單元進(jìn)行檢查和驗(yàn)證。在結(jié)構(gòu)化編程的時(shí)代,單元測試中單元指的就是函數(shù)。beeshell 組件庫全面使用單元測試,由組件的開發(fā)者完成。研究成果表明,無論什么時(shí)候作出修改都需要進(jìn)行完整的回歸測試,對于提供基礎(chǔ)功能的組件來說更是如此,在生命周期中盡早地對軟件產(chǎn)品進(jìn)行測試將使效率和質(zhì)量都得到最好的保證。Bug 發(fā)現(xiàn)的越晚,修改它所需的成本就越高,單元測試是一個(gè)在早期抓住 Bug 的機(jī)會(huì)。
單元測試的優(yōu)點(diǎn)有以下幾點(diǎn):
是一種驗(yàn)證行為。程序的每一項(xiàng)功能是測試來驗(yàn)證正確性,為后期的增加功能、代碼重構(gòu)提供了保障。
是一種設(shè)計(jì)行為。單元測試使得我們從調(diào)用者的角度觀察、思考,迫使開發(fā)者把程序設(shè)計(jì)成易于調(diào)用和可測試的,在一定程度上降低耦合性。
是一種編寫文檔的行為。是展示函數(shù)、類使用的最佳文檔。
beeshell 組件庫使用 Jest 做為單元測試的工具,自帶斷言、測試覆蓋率工具,實(shí)現(xiàn)開箱即用。
測試用例設(shè)計(jì)
測試用例的核心是輸入數(shù)據(jù),我們會(huì)選擇具有代表性的數(shù)據(jù)作為輸入數(shù)據(jù),主要有三種:正常輸入,邊界輸入,非法輸入,下面以組件庫中提供的 isLeapYear 工具函數(shù)來舉例說明,代碼如下:
Jest 使用 test 函數(shù)來描述一個(gè)測試用例,其中的 toBe 邊是一句斷言。
函數(shù)使用了外部數(shù)據(jù),正常輸入肯定會(huì)有,這里的 2000 和 '2000' 都是正常輸入;邊界輸入和非法輸入并不是所有的函數(shù)都有,這里為了說明使用了有這兩種輸入的例子,邊界輸入是有效輸入的極限值,這里 0和 Infinity 是邊界輸入;非法輸入是正常取值范圍以外的數(shù)據(jù), 'xx' 和 false 則是非法輸入。一般情況下,考慮以上三種輸入可以找出函數(shù)的基本功能點(diǎn),單元測試與代碼編寫是“一體兩面”的關(guān)系,編碼時(shí)對上述三種輸入都是應(yīng)該考慮的,否則代碼的健壯性就會(huì)出現(xiàn)問題。
上文所說的測試是針對程序的功能來設(shè)計(jì)的,就是所謂的“黑盒測試”。單元測試還需要從另一個(gè)角度來設(shè)計(jì)測試數(shù)據(jù),即針對程序的邏輯結(jié)構(gòu)來設(shè)計(jì)測試用例,就是所謂的“白盒測試”。
還是以 isLeapYear 函數(shù)來進(jìn)行說明,其代碼如下:
這里有一個(gè) if else 語句,如果我們只提供一個(gè) 2000 的輸入,只會(huì)測試到 if 語句,而不會(huì)測試 else 語句。雖然,在黑盒測試足夠充分的情況下,白盒測試沒有必要,可惜“足夠充分”只是一種理想狀態(tài),難于衡量測試的完整性是黑盒測試的主要缺陷。而白盒測試恰恰具有易于衡量測試完整性的優(yōu)點(diǎn),兩者之間具有極好的互補(bǔ)性,例如:完成功能測試后統(tǒng)計(jì)語句覆蓋率,如果語句覆蓋未完成,很可能是未覆蓋的語句所對應(yīng)的功能點(diǎn)未測試。
白盒測試也是比較常見的需求,Jest 內(nèi)置了測試覆蓋率工具,可以直接在命令中添加 --coverage 參數(shù)便可以輸出單元測試覆蓋率的報(bào)告,結(jié)果如下:
可以看到代碼的每一行都覆蓋到了 Coverage 為 100%,在很大程度上保證了功能的穩(wěn)定性。
UI 自動(dòng)化測試
想要確保組件庫的 UI 不會(huì)意外被更改,快照測試(Snapshot Testing)是非常有用的工具。一個(gè)典型的移動(dòng) App 快照測試案例過程是,先渲染 UI 組件,然后截圖,最后和獨(dú)立于測試存儲(chǔ)的參考圖像進(jìn)行比較。使用 Jest 進(jìn)行在快照測試,在 beeshell 中第一次對某個(gè)組件進(jìn)行測試時(shí),會(huì)在測試目錄下創(chuàng)建一個(gè) snapshots 文件夾,并將快照結(jié)果存放在該文件夾中。快照結(jié)果文件以 <組件名>.js.snap 命名,其內(nèi)容為某個(gè)狀態(tài)下的 UI 組件樹。
下面以 Button 組件快照測試為例來說明:
運(yùn)行命令后得到快照結(jié)果:
靜態(tài)分析
經(jīng)常與單元測試聯(lián)系起來的開發(fā)活動(dòng)還有靜態(tài)分析(Static analysis)。靜態(tài)分析就是對軟件的源代碼進(jìn)行研讀,查找錯(cuò)誤或收集一些度量數(shù)據(jù),并不需要對代碼進(jìn)行編譯和執(zhí)行。
靜態(tài)分析效果較好而且快速,可以發(fā)現(xiàn) 30%~70% 的代碼問題,可以在幾分鐘內(nèi)檢查一遍,成本低、收益高。beeshell 使用 SonarQube 進(jìn)行靜態(tài)代碼檢查。
SonarQube 是一個(gè)開源的代碼質(zhì)量管理系統(tǒng),支持 25+ 種語言,可以通過使用插件機(jī)制與 Eclipse、VSCode 等工具集成,實(shí)現(xiàn)對代碼的質(zhì)量的全面自動(dòng)化分析和管理。
SonarQube 通過對 Reliability(可靠性)、Security(安全性)、Maintainability(可維護(hù)性)、Coverage(測試覆蓋率)、Duplications(重復(fù))幾個(gè)維度,對代碼進(jìn)行全方位的分析,通過設(shè)置 Quality Gates 保證代碼質(zhì)量。
beeshell 組件庫的分析結(jié)果概況如圖:
可靠性達(dá)到 A 級(jí)別,是最高等級(jí),表示無 Bug:
安全性達(dá)到 A 級(jí)別,是最高等級(jí),表示無漏洞:
測試覆蓋率平均達(dá)到 70% 以上
開發(fā)與使用一致性
beeshell 組件庫使用 npm 包的形式下載使用,下載成功后會(huì)放置在項(xiàng)目根目錄的 node_modules 目錄,然后在項(xiàng)目中通過引入模塊的方式,引入 beeshell 的組件來使用。
那我們?nèi)绾伍_發(fā)組件庫?如何保證組件庫的開發(fā)與使用的體驗(yàn)一致性?
首先,我們需要一個(gè) demo 項(xiàng)目,這個(gè)項(xiàng)目是 beeshell 組件庫的開發(fā)環(huán)境,是一個(gè) React Native 應(yīng)用。然后,我們把 beeshell 做為 demo 項(xiàng)目的依賴,在 demo 項(xiàng)目中下載安裝。
現(xiàn)在,我們的問題就變成了 node_modules 目錄中的 beeshell 如何和本地的 beeshell 源碼進(jìn)行同步。
npm link
我們知道可以使用 npm link 來開發(fā) npm 包,原理如下:
本質(zhì)是就是使用 Symbol link,但是我們建立好軟鏈接后,運(yùn)行打包命令卻報(bào)錯(cuò)了,錯(cuò)誤信息為 Expected path '/xxx/xxx/index.js' to be relative to one of project roots
我們前端開發(fā)通常會(huì)用 Webpack 做為打包工具,而 React Native 應(yīng)用使用的是 Metro,我們需要分析 Metro 來定位問題。
Webpack vs Metro
經(jīng)過 Metro 的源碼分析,我們發(fā)現(xiàn) Metro 的打包方案與 Webpack 有較大差異,Webpack 是根據(jù)入口文件,即配置中的 entry 屬性,遞歸解析依賴,構(gòu)建依賴關(guān)系圖而 Metro 是爬取特定路徑下的所有文件來構(gòu)建依賴關(guān)系圖。
分析發(fā)現(xiàn) Metro 的特定路徑默認(rèn)是運(yùn)行打包命里的路徑,以及 node_modules 下第一層目錄,這樣我們就定位到了問題的所在:
Metro 在爬取文件的時(shí)候,通過軟鏈接找到了全局的 beeshell 但是并沒有繼續(xù)判斷全局的 beeshell 是否有軟鏈接,所以無法爬取 beeshell 源碼部分。
直接使用軟鏈接
通過 ln -s 命令,直接建立 demo 項(xiàng)目 node_modules 下 beeshell 包 與 beeshell 源碼的軟鏈接:
這種方式同時(shí)支持 Native 部分 iOS、Android 的源碼開發(fā),注意 Android 部分的需要在 setting.gradle 中調(diào)用 getCanonicalPath 方法獲取建立軟鏈接后的路徑。
通過試驗(yàn)、發(fā)現(xiàn)問題、分析源碼、定位問題、解決問題、方案完善這幾個(gè)步驟,完整的實(shí)現(xiàn)了 beeshell 組件庫的開發(fā)與使用的體驗(yàn)一致性,同時(shí)提升了組件庫的開發(fā)效率。
未來展望
我們的目標(biāo)是把 beeshell 建設(shè)成為一個(gè)大而全的組件庫,不僅會(huì)不斷豐富 JS 組件,而且會(huì)不斷加強(qiáng)復(fù)合組件去支持更多的底層功能。因?yàn)槲覀冎С秩恳牒桶葱枰雰煞N方式,用戶不需要擔(dān)心會(huì)引入過多無用組件而使得包體積過大,影響開發(fā)和使用效率。
beeshell 目前提供了 20+ 組件以及基礎(chǔ)工具,基于良好的架構(gòu)設(shè)計(jì)、開發(fā)體驗(yàn),為我們不斷地豐富組件庫提供了良好的基礎(chǔ)。同時(shí)在開發(fā) React Native 應(yīng)用的幾年時(shí)間中,我們已經(jīng)積累了 50+ 基礎(chǔ)以及業(yè)務(wù)組件,我們后續(xù)會(huì)把積累的組件進(jìn)行梳理與調(diào)整,全部遷移到 beeshell 中。因?yàn)槲覀兊慕M件主要來源于我們的業(yè)務(wù)需求,但是業(yè)務(wù)場景有限,可能會(huì)使得 beeshell 的發(fā)展受到限制,所以我們將其開源。希望借助社區(qū)的力量不斷豐富組件庫的功能,盡最大努力覆蓋到移動(dòng)應(yīng)用方方面面的功能,歡迎大家獻(xiàn)計(jì)獻(xiàn)策,多多支持。
我們?yōu)榻M件庫發(fā)展規(guī)劃了三個(gè)階段:
第一階段,即我們現(xiàn)在所處的階段,開源 20+ 組件,主要提供基礎(chǔ)功能。
第二階段,對我們在開發(fā) React Native 應(yīng)用幾年時(shí)間積累的組件進(jìn)行整理,開源 50+ 組件。
第三階段,調(diào)研移動(dòng)端 App 常用的功能,分析與整理,然后在 beeshell 中實(shí)現(xiàn),開源 100+ 組件。
