作者:何樂樂
前言
對于中大型移動端APP開發(fā)來講,組件化是一種常用的項目架構(gòu)方式。個人最近幾年在工作項目中也一直使用組件化的方式來開發(fā),在這過程中也積累了一些經(jīng)驗和思考。主要是來自在日常開發(fā)中使用組件化開發(fā)遇到的問題以及和其他開發(fā)同學(xué)的交流探討。
本文通過以下問題來介紹組件化這種開發(fā)架構(gòu)的思想和常見的一些問題:
- 為什么需要組件化
- 組件化過程中會遇到的挑戰(zhàn)和選擇
- 如何維護(hù)一個高質(zhì)量的組件化項目
提示:本文說的組件化工程是指
Multirepo使用獨(dú)立的git倉庫來管理組件。
組件化可以帶來什么
單一工程架構(gòu)遇到的問題
在組件化架構(gòu)之前,傳統(tǒng)使用的工程架構(gòu)主要是以Monolithic方式的單一工程架構(gòu),也就是將所有代碼放在單個代碼倉庫里管理。單一工程架構(gòu)使用了這么多年為什么突然遇到了問題,這也引入了APP項目開發(fā)的一個大背景,現(xiàn)有中大型APP項目變的越來越復(fù)雜:
-
多APP項目并存- 集團(tuán)內(nèi)部存在多個APP項目,不同APP希望可以復(fù)用現(xiàn)有組件能力快速搭建出新的APP。 -
功能增多- 隨著項目功能越來越多,代碼量增多。同時需要更多的開發(fā)人員參與到項目中,這會增加開發(fā)團(tuán)隊之間協(xié)作的成本。 -
多語言/多技術(shù)棧- 引入了更多的新技術(shù),例如使用一種以上的跨平臺UI技術(shù)用于快速交付業(yè)務(wù),不同的編程語言、音視頻、跨平臺框架,增加了整個工程的復(fù)雜度。
以上這些業(yè)務(wù)發(fā)展的訴求就給傳統(tǒng)單一工程架構(gòu)方式帶來了很多新的技術(shù)要求:
工程效率
- 工程代碼量過大會導(dǎo)致
編譯速度緩慢。 - 單
git工程提交同時可能帶來更多的git提交沖突和編譯錯誤。
質(zhì)量問題
- 如何將
git提交關(guān)聯(lián)到對應(yīng)的功能模塊需求。發(fā)版時進(jìn)行合規(guī)檢查避免帶入不規(guī)范的代碼,對整個功能模塊回滾的訴求。 - 如何在單倉庫中管控這么多開發(fā)人員的代碼權(quán)限,盡可能避免不安全的提交并且限制改動范圍。
更大范圍的組件復(fù)用
- 基礎(chǔ)組件從支持
單個APP復(fù)用到支持多個APP復(fù)用。 - 不只是
基礎(chǔ)能力組件,對于業(yè)務(wù)能力組件也需要支持復(fù)用。(例如一個頁面組件同時在多個APP使用) -
跨平臺容器需要復(fù)用底層組件能力避免重復(fù)開發(fā),同時不同跨平臺容器API需要盡量保持統(tǒng)一,底層基礎(chǔ)設(shè)施向容器化發(fā)展支持業(yè)務(wù)跨APP復(fù)用。
跨技術(shù)棧通信
- 由于頁面導(dǎo)航多技術(shù)?;旌瞎泊?,頁面路由需要支持跨技術(shù)棧。
-
跨組件通信需要支持跨語言/跨技術(shù)棧通信。
更好的解耦
- 頁面解耦。由于頁面導(dǎo)航棧混合共存,頁面自身不再清晰的知道上游和下游頁面由什么技術(shù)棧搭建,所以頁面路由需要做到
完全解耦隔離技術(shù)棧的具體實現(xiàn)。 - 業(yè)務(wù)組件間維持松耦合關(guān)系,可以靈活
添加/移除,基于現(xiàn)有組件能力快速搭建出不同的APP。 - 對于同一個服務(wù)或頁面可以插件化方式靈活提供多種不同的實現(xiàn),不同的APP宿主也可以提供不同的實現(xiàn)并且提供
A/B能力。 - 由于
包體積限制和不同組件包含相同符號導(dǎo)致的符號沖突問題,在復(fù)用組件的時候需要盡可能引入最小依賴原則降低接入成本。
組件化架構(gòu)的優(yōu)勢
基于以上這些問題,現(xiàn)在的組件化架構(gòu)希望可以解決這些問題提升整個交付效率和交付質(zhì)量。
組件化架構(gòu)通常具備以下優(yōu)點(diǎn):
-
代碼復(fù)用- 功能封裝成組件更容易復(fù)用到不同的項目中,直接復(fù)用可以提高開發(fā)效率。并且每個組件職責(zé)單一使用時會帶入最小的依賴。 -
降低理解復(fù)雜度- 工程拆分為小組件以后,對于組件使用方我們只需要通過組件對外暴露的公開API去使用組件的功能,不需要理解它內(nèi)部的具體實現(xiàn)。這樣可以幫助我們更容易理解整個大的項目工程。 -
更好的解耦- 在傳統(tǒng)單一工程項目中,雖然我們可以使用設(shè)計模式或者編碼規(guī)范來約束模塊間的依賴關(guān)系,但是由于都存放在單一工程目錄中缺少清晰的模塊邊界依然無法避免不健康的依賴關(guān)系。組件化以后可以明確定義需要對外暴露的能力,對于模塊間的依賴關(guān)系我們可以進(jìn)行強(qiáng)約束限制依賴,更好的做到解耦。對一個模塊的添加和移除都會更容易,并且模塊間的依賴關(guān)系更加清晰。 -
隔離技術(shù)棧- 不同的組件可以使用不同的編程語言/技術(shù)棧,并且不用擔(dān)心會影響到其他組件或主工程。例如在不同的組件內(nèi)可以自由選擇使用Kotlin或Swift,可以使用不同的跨平臺框架,只需要通過規(guī)范的方式暴露出頁面路由或者服務(wù)方法即可。 -
獨(dú)立開發(fā)/維護(hù)/發(fā)布- 大型項目通常有很多團(tuán)隊。在傳統(tǒng)單一項目集成打包時可能會遇到代碼提交/分支合并的沖突問題。組件化以后每個團(tuán)隊負(fù)責(zé)自己的組件,組件可以獨(dú)立開發(fā)/維護(hù)/發(fā)布提升開發(fā)效率。 -
提高編譯/構(gòu)建速度- 由于組件會提前編譯發(fā)布成二進(jìn)制庫進(jìn)行依賴使用,相比編譯全部源代碼可以節(jié)省大量的編譯耗時。同時在日常組件開發(fā)時只需要編譯少量依賴組件,相比單一工程可以減少大量的編譯耗時和編譯錯誤。 -
管控代碼權(quán)限- 通過組件化將代碼拆分到不同組件git倉庫中,我們可以更好的管控代碼權(quán)限和限制代碼變更范圍。 -
管理版本變更- 我們通常會使用CocoaPods/Gradle這類依賴管理工具來管理項目中所有的組件依賴。因為每一個組件都有一個明確的版本,這樣我們可以通過對比APP不同版本打包時的組件依賴表很清晰的識別組件版本特性的變更,避免帶入不合規(guī)的組件版本特性。并且在出現(xiàn)問題時也很方便通過配置表進(jìn)行回滾撤回。
提示:組件化架構(gòu)是為了解決
單一工程架構(gòu)開發(fā)中的問題。如果你的項目中也會遇到這些痛點(diǎn),那可能就需要做組件化。
組件化遇到的挑戰(zhàn)
雖然組件化架構(gòu)可以帶來這么多收益,但不是只要使用組件化架構(gòu)就可以解決所有問題。通常來講當(dāng)我們使用一種新的技術(shù)方案解決現(xiàn)有問題的時候也會帶來一些新的問題,組件化架構(gòu)能帶來多少收益主要取決于整個工程組件化的質(zhì)量。那在組件化架構(gòu)中我們?nèi)绾稳ピu估項目工程的組件化架構(gòu)質(zhì)量,我們需要關(guān)注哪些問題。對于軟件架構(gòu)來講,最重要的就是管理組件實體以及組件間的關(guān)系。所以對于組件化架構(gòu)來講主要是關(guān)注以下三個問題:
- 如何劃分組件的粒度、組件職責(zé)邊界在哪里?
- 組件間的依賴關(guān)系應(yīng)該如何管理?
- 組件間應(yīng)該使用哪種方式調(diào)用和通信?
1. 組件拆分的粒度、組件職責(zé)邊界在哪里?
某種程度上組件拆分粒度也是一種平衡的藝術(shù),我們需要在效率和質(zhì)量之間找到一種相對的平衡。組件拆分粒度太粗:導(dǎo)致組件間耦合緊密,并不能利用更好的復(fù)用/解耦/提高編譯速度這些優(yōu)勢。組件拆分粒度太細(xì):導(dǎo)致需要維護(hù)更多的組件代碼倉庫、功能變更可能涉及多個組件代碼的修改/發(fā)布,這些都會帶來額外的成本,同時組件過多也會導(dǎo)致組件依賴查找過程變的更復(fù)雜更慢。
組件的職責(zé)也會影響我們對于組件的拆分方式:每個組件的定位是什么,應(yīng)該包含什么樣的功能,是否可以被復(fù)用,添加某個功能的時候應(yīng)該創(chuàng)建新組件還是添加到現(xiàn)有組件,當(dāng)組件復(fù)雜到一定程度時是否需要拆分出新個組件。
在拆分組件前需要提前去思考這些問題。
2. 組件間的依賴關(guān)系應(yīng)該如何管理?
組件間的依賴方式主要分為直接強(qiáng)耦合依賴和間接松耦合依賴。強(qiáng)耦合依賴是對依賴的組件直接使用對應(yīng)的API進(jìn)行調(diào)用,這種調(diào)用方式優(yōu)點(diǎn)是簡單直接性能更好,缺點(diǎn)是一種完全耦合的調(diào)用方式。(基礎(chǔ)組件通常使用這種方式)。松耦合依賴主要是通過通知、URL Scheme、ObjC Runtime、服務(wù)接口、事件隊列等通信方式進(jìn)行間接依賴調(diào)用。雖然性能相對差一點(diǎn),但這是一種相對耦合程度比較低并且靈活的依賴方式。(業(yè)務(wù)組件通常使用這種方式)
組件間的依賴關(guān)系很重要是因為在長期的項目開發(fā)演化過程中很容易形成一種復(fù)雜的網(wǎng)狀依賴關(guān)系。雖然看似使用組件化的方式將模塊拆分成不同的組件,但是組件間可能存在很多相互交叉的依賴耦合關(guān)系,很多組件都被其他組件直接依賴或隱式間接依賴。這樣我們就背離了組件化架構(gòu)更好的解耦、更好的復(fù)用、更快速的開發(fā)/編譯/發(fā)布的初衷。
所以我們需要制定一套規(guī)范去約束和規(guī)范組件間的依賴關(guān)系:兩個組件之間是否可以依賴,組件間依賴方向,選擇強(qiáng)耦合依賴還是松耦合依賴。
3. 組件間松耦合依賴關(guān)系應(yīng)該使用哪種方式調(diào)用和通信?
松耦合依賴通常可以使用通知、URL Scheme、ObjC Runtime、服務(wù)接口、事件隊列等方式通信進(jìn)行間接調(diào)用,但是使用哪種方式更好業(yè)界也有很多爭論,并且每種方式都有一些優(yōu)缺點(diǎn)。通常在項目中會根據(jù)不同的使用場景至少會選擇2種通信方式。
耦合程度低的方式例如URL Scheme,可以做到完全解耦相對比較靈活。但是無法利用編譯時檢查、無法傳遞復(fù)雜對象、調(diào)用方/被調(diào)用方都需要對參數(shù)做大量的正確性檢查和對齊。同時可能無法檢測對應(yīng)的調(diào)用方法是否存在。
耦合程度高的方式例如服務(wù)接口,需要對服務(wù)接口方法進(jìn)行強(qiáng)依賴,但是可以利用編譯時檢查、傳遞復(fù)雜對象、并且可以更好的支持Swift特性。
我們需要在解耦程度、容易使用、安全上找到一種合適的方式。
提示:這里的耦合程度高是相對于耦合程度低的方式進(jìn)行比較,相比
直接依賴對應(yīng)組件依然是一種耦合程度低的依賴關(guān)系。
組件化架構(gòu)實踐規(guī)范和原則
基于以上這些組件化架構(gòu)的問題,需要一些組件化架構(gòu)相關(guān)的規(guī)范和原則幫助我們做好組件化架構(gòu),后面主要會圍繞以下三點(diǎn)進(jìn)行介紹:
-
組件拆分原則- 拆分思想和最佳實踐指導(dǎo)組件拆分 -
組件間依賴- 優(yōu)化組件間依賴關(guān)系跨組件調(diào)用/通信方式的選擇 -
質(zhì)量保障- 避免在持續(xù)的工程演化過程中工程質(zhì)量逐漸劣化。主要包含安全卡口和CI檢查
工程實例
接下來以一個典型的電商APP架構(gòu)案例來介紹一個組件化工程。這個案例架構(gòu)具備之前所說現(xiàn)有中大型APP架構(gòu)的一些特點(diǎn),多組件、多技術(shù)棧、業(yè)務(wù)間需要解耦、復(fù)用底層基礎(chǔ)組件。基于這個案例來介紹上面的三點(diǎn)原則。

組件拆分原則

組件拆分最重要是幫我們梳理出組件職責(zé)以及組件職責(zé)的邊界。組件劃分也會使用很多通用的設(shè)計原則和架構(gòu)思想。
使用分層思想拆分
通常我們可以首先使用分層架構(gòu)的思想將所有組件縱向拆分為多層組件,上面層級的組件只能依賴下面層級的組件。一般至少可以劃分為四層組件:
-
基礎(chǔ)層- 提供核心的與上層業(yè)務(wù)無關(guān)的基礎(chǔ)能力??梢员簧蠈咏M件直接依賴使用。 -
業(yè)務(wù)公共層- 主要包含頁面路由、公共UI組件、跨組件通信以及服務(wù)接口,可被上層組件直接依賴使用。 -
業(yè)務(wù)實現(xiàn)層- 業(yè)務(wù)核心實現(xiàn)層,包含原生頁面、跨平臺容器、業(yè)務(wù)服務(wù)實現(xiàn)。組件間不能直接依賴,只能通過調(diào)用頁面路由或跨組件通信組件進(jìn)行使用。 -
APP宿主層- 主要包含APP主工程、啟動流程、頁面路由注冊、服務(wù)注冊、SDK參數(shù)初始化等組件,用于構(gòu)建打包生成相應(yīng)的APP。
劃分層級可以很好的指導(dǎo)我們進(jìn)行組件拆分。在拆分組件時我們需要先識別它應(yīng)該在哪一層,它應(yīng)該以哪種調(diào)用方式被其他組件使用,新添加的功能是否會產(chǎn)生反向依賴,幫助我們規(guī)范組件間的依賴關(guān)系。同時按層級拆分組件也有利于底層基礎(chǔ)組件的復(fù)用。
以下場景使用分層思想就很容易識別:
基礎(chǔ)組件依賴業(yè)務(wù)組件
例子:APP內(nèi)業(yè)務(wù)發(fā)起網(wǎng)絡(luò)請求通常需要攜帶公共參數(shù)/Cookie。
-
沒有組件分層約束- 網(wǎng)絡(luò)庫可能會依賴登錄服務(wù)獲取用戶信息、依賴定位服務(wù)獲取經(jīng)緯度,引入大量的依賴變成業(yè)務(wù)組件。 -
有組件分層約束- 網(wǎng)絡(luò)庫作為一個基礎(chǔ)組件,它不需要關(guān)注上層業(yè)務(wù)需要攜帶哪些公共業(yè)務(wù)參數(shù),同時登錄/定位服務(wù)組件在網(wǎng)絡(luò)庫上層不能被反向依賴。這時候會考慮單獨(dú)創(chuàng)建一個公共參數(shù)管理類,在APP運(yùn)行時監(jiān)聽各種狀態(tài)的變更并調(diào)用網(wǎng)絡(luò)庫更新公共參數(shù)/Cookie。
業(yè)務(wù)組件間依賴方向是否正確
登錄狀態(tài)切換經(jīng)常會涉及到很多業(yè)務(wù)邏輯的觸發(fā),例如清空本地用戶緩存、地址緩存、清空購物車數(shù)據(jù)、UI狀態(tài)變更。
-
沒有組件分層約束- 可能會在登錄服務(wù)內(nèi)當(dāng)?shù)卿洜顟B(tài)切換時調(diào)用多個業(yè)務(wù)邏輯的觸發(fā),導(dǎo)致登錄服務(wù)引入多個業(yè)務(wù)組件依賴。 -
有組件分層約束- 登錄組件只需要在登錄狀態(tài)切換時發(fā)出通知,無需知道登錄狀態(tài)切換會影響哪些業(yè)務(wù)。業(yè)務(wù)邏輯應(yīng)該監(jiān)聽登錄狀態(tài)的變更。
識別基礎(chǔ)組件還是業(yè)務(wù)組件
雖然很多場景下我們很容易能識別處理出來一個功能應(yīng)該歸屬于基礎(chǔ)組件還是業(yè)務(wù)組件,例如一個UI控件是基礎(chǔ)組件還是業(yè)務(wù)組件。但是很多時候邊界又非常的模糊,例如一個添加購物車按鍵應(yīng)該是一個基礎(chǔ)組件還是業(yè)務(wù)組件呢。
-
基礎(chǔ)組件- 如果不需要依賴業(yè)務(wù)公共層那應(yīng)當(dāng)劃分為一個基礎(chǔ)組件。 -
業(yè)務(wù)組件- 依賴了業(yè)務(wù)公共層或者網(wǎng)絡(luò)庫,那就應(yīng)該劃分為一個業(yè)務(wù)組件。
分層思想可以很好的幫助我們管理組件間的依賴關(guān)系,并且明確每個組件的職責(zé)邊界。
基礎(chǔ)/業(yè)務(wù)組件拆分原則
劃分基礎(chǔ)/業(yè)務(wù)組件主要是為了強(qiáng)制約束組件間的依賴關(guān)系。以上面的組件分層架構(gòu)為例:
-
基礎(chǔ)組件- 基礎(chǔ)組件可被直接依賴使用,使用方調(diào)用基礎(chǔ)組件對外暴露API直接使用。基礎(chǔ)層、業(yè)務(wù)公共層都為基礎(chǔ)組件。 -
業(yè)務(wù)組件- 業(yè)務(wù)組件不可被直接依賴使用,只能通過間接通信方式進(jìn)行使用。APP宿主層和業(yè)務(wù)實現(xiàn)層都為業(yè)務(wù)組件。
提示:這里的業(yè)務(wù)組件并不包含
業(yè)務(wù)UI組件。
基礎(chǔ)組件拆分
基礎(chǔ)組件通常根據(jù)職責(zé)單一原則進(jìn)行拆分比較容易拆分,但是會有一些拆分場景需要考慮:
使用插件組件拆分基礎(chǔ)組件擴(kuò)展能力
將核心基礎(chǔ)能力和擴(kuò)展能力拆分到不同的組件。以網(wǎng)絡(luò)庫為例,除了提供最核心的接口請求能力,同時可能還包含一些擴(kuò)展能力例如HTTPDNS、網(wǎng)絡(luò)性能檢測、弱網(wǎng)優(yōu)化等能力。但這些擴(kuò)展能力放在網(wǎng)絡(luò)庫組件內(nèi)部可能會導(dǎo)致以下問題:
- 擴(kuò)展能力會使組件自身代碼變的更加復(fù)雜。
- 使用方不一定會使用所有這些擴(kuò)展能力違反了
最小依賴原則。帶來更多的包體積,引入更多的組件依賴,增加模塊間的耦合度。 - 相關(guān)的擴(kuò)展能力不支持靈活的替換/插拔。
所以這種場景我們可以考慮根據(jù)實際情況將擴(kuò)展能力拆分到相應(yīng)的插件組件,使用方需要時再依賴引入對應(yīng)插件組件。
業(yè)務(wù)組件拆分
業(yè)務(wù)頁面拆分方式
針對業(yè)務(wù)頁面可以使用技術(shù)棧、業(yè)務(wù)域、頁面粒度三種方式進(jìn)行更細(xì)粒度的劃分,通常至少要拆分到技術(shù)棧、業(yè)務(wù)域這一層級,頁面粒度拆分根據(jù)具體頁面復(fù)雜度和復(fù)用訴求。
-
基于技術(shù)棧進(jìn)行拆分- 不同的技術(shù)棧需要拆分到不同的組件進(jìn)行管理。 -
基于業(yè)務(wù)域進(jìn)行拆分- 將同一個業(yè)務(wù)域的所有頁面拆分一個組件,避免不同業(yè)務(wù)域之間形成強(qiáng)耦合依賴關(guān)系,同一個業(yè)務(wù)域通常會有更多復(fù)用和通信的場景也方便開發(fā)。例如訂單詳情和訂單列表可放置在一起管理。 -
基于頁面粒度進(jìn)行拆分- 單個頁面復(fù)雜度過高或需要被單獨(dú)復(fù)用時需要拆分到一個單個組件管理。
提示:放置在單一組件內(nèi)的多個頁面之間也應(yīng)適當(dāng)降低耦合程度。
第三方庫
第三方庫應(yīng)拆分單獨(dú)組件管理
第三方庫應(yīng)使用獨(dú)立的組件進(jìn)行管理,一方面有利于組件復(fù)用同時避免多個重復(fù)第三方庫導(dǎo)致符號沖突,另一方面有利于后續(xù)升級維護(hù)。
一些提示
減少使用通用聚合公共組件
為了避免拆分過多的組件,我們通常會創(chuàng)建聚合組件將一些代碼量不多/功能相似的類放到同一個組件內(nèi),例如Foundation組件、UI組件。但是很多時候會存在濫用的場景,應(yīng)當(dāng)警惕這類公共聚合組件。下面是一些公共聚合組件容易濫用的場景:
- 添加一個新功能不知道應(yīng)當(dāng)加在哪里時,就加到公共聚合組件內(nèi),時間久了以后公共組件依賴特別多。
- 公共組件添加了一個非常復(fù)雜的能力,導(dǎo)致復(fù)雜度變高或者引入大量依賴
- 太多能力聚合到一起。例如將網(wǎng)絡(luò)庫、圖片庫這些能力放在同一個組件內(nèi)
- 基礎(chǔ)/業(yè)務(wù)UI組件沒有拆分?;A(chǔ)UI組件通常只提供最基礎(chǔ)的UI和非常輕量的邏輯,業(yè)務(wù)組件通常會充當(dāng)基礎(chǔ)UI組件的數(shù)據(jù)源以及業(yè)務(wù)邏輯。
但是也不能完全避免使用聚合公共組件,不然會導(dǎo)致產(chǎn)生更多的小組件增加維護(hù)成本。但是我們將一個能力添加到公共聚合組件時可以根據(jù)以下幾個條件來權(quán)衡:
- 是否會引入大量新的依賴
- 功能復(fù)雜度、代碼數(shù)量,太復(fù)雜的不應(yīng)該添加到公共組件
- 能力是否需要被單獨(dú)復(fù)用,需要單獨(dú)復(fù)用就不應(yīng)該添加到公共組件
第三方庫考慮不直接對外暴露使用
當(dāng)存在以下情況時可考慮對第三方庫進(jìn)行適當(dāng)?shù)姆庋b避免直接暴露第三方庫:
- 使用方通常只需要使用少量API,第三方庫會對外暴露大量API增加使用難度,同時可能導(dǎo)致一些安全問題
- 對外隱藏具體實現(xiàn),方便后續(xù)更換其他第三方庫、自實現(xiàn)、第三方庫發(fā)生
Break Change變更時升級更容易 - 需要封裝擴(kuò)展一些能力讓使用方使用起來更容易
以網(wǎng)絡(luò)庫為例:
1.通常需要對接公司內(nèi)部的API網(wǎng)關(guān)能力所以需要適當(dāng)做一些封裝,例如簽名或者加密策略。
2.使用方通常只需要用到一個通用的請求方法無需對外暴露太多API。
3.為了安全通常需要對業(yè)務(wù)方隱藏一些方法避免錯誤調(diào)用,例如全局Cookie修改等能力。
4.對外隱藏具體第三方庫可以方便變更。
第三方庫盡可能避免直接修改源碼
第三方庫組件盡可能不要直接修改源碼,除修復(fù)Bug/Crash之外盡可能避免帶入其他功能代碼導(dǎo)致后面更新困難。需要添加功能時可以通過在其他組件內(nèi)使用第三方庫對外暴露的API進(jìn)行能力擴(kuò)展。
組件間依賴關(guān)系
業(yè)務(wù)組件間通信方式選擇
松耦合通信方式對比

基于以上表格中各種方案的優(yōu)缺點(diǎn),個人推薦使用URL Scheme協(xié)議作為頁面路由通信方式,使用服務(wù)接口提供業(yè)務(wù)功能服務(wù)。通知訂閱場景可使用通知或RxSwift方式提供一對多的訂閱能力。
服務(wù)接口
服務(wù)接口對應(yīng)的實現(xiàn)和頁面是否需要拆分
以購物車服務(wù)為例,購物車接口服務(wù)提供了添加購物車的能力。加車服務(wù)具體的實現(xiàn)應(yīng)該放在購物車頁面組件內(nèi)還是獨(dú)立出來放置在單獨(dú)的組件。將購物車服務(wù)實現(xiàn)和購物車頁面拆分的優(yōu)點(diǎn)是購物車服務(wù)和購物車頁面更好的解耦,都能單獨(dú)支持復(fù)用。缺點(diǎn)是開發(fā)效率降低,修改購物車功能時可能會涉及到同時修改購物車服務(wù)組件和購物車頁面組件。
所以在需要單獨(dú)復(fù)用服務(wù)或頁面的場景時可考慮分別拆分出單個組件(例如購物車服務(wù)作為一種通用能力提供給上層跨平臺容器能力)。但即使在同一個組件內(nèi)也建議對服務(wù)和頁面使用分層設(shè)計的方式進(jìn)行解耦。
服務(wù)接口是否需要拆分
一般項目可能至少會有10+個服務(wù)接口,這些服務(wù)接口應(yīng)該統(tǒng)一存放在單個組件還是每個接口對應(yīng)一個組件。
- 統(tǒng)一存放:優(yōu)點(diǎn)是一起管理更快捷方便。缺點(diǎn)是所有接口對應(yīng)一個組件版本,不能支持單一接口使用不同版本,不利于需要
跨APP復(fù)用的項目。并且使用方可能會引入大量無用的接口依賴。 - 分開存放:優(yōu)點(diǎn)是每個接口可使用不同的版本并且使用方只需要依賴特定的接口。缺點(diǎn)是會產(chǎn)生更多的組件倉庫,組件數(shù)量也會增加依賴查找的耗時。 所以大型項目選擇分開存放的方式管理接口相對更合適一點(diǎn)。也可以考慮將大部分最核心的服務(wù)接口放置到一起管理。
支持Swift的服務(wù)接口實現(xiàn)推薦
使用Swift實現(xiàn)傳統(tǒng)的服務(wù)接口模式通常會遇到以下兩個問題:
- 接口需要同時支持
Objective-C和Swift調(diào)用,同時希望使用Swift特性設(shè)計API。如何實現(xiàn)Objective-C和Swift協(xié)議可以復(fù)用一個實例 -
Swift對于動態(tài)性支持比較弱,純Swift類無法支持運(yùn)行時動態(tài)創(chuàng)建只能在注冊時創(chuàng)建實例
基于以上問題,個人推薦使用下面的方式實現(xiàn)接口服務(wù)模式:
- 使用
Objective-C協(xié)議提供最基礎(chǔ)的服務(wù)能力,之后創(chuàng)建Swift協(xié)議擴(kuò)展提供部分Swift特性的API - 接口實現(xiàn)類繼承
NSObject支持運(yùn)行時動態(tài)初始化
// @objc協(xié)議
@objc public protocol JDCartService {
func addCart(request: JDAddCartRequest, onSuccess: () -> Void, onFail: () ->)
}
// swift協(xié)議
public protocol CartService: JDCartService {
func addCart() async
func addCart(onCompletion: Result<Data, Error>)
}
// 實現(xiàn)類
class CartServiceImp: NSObject, CartService {
// 同時實現(xiàn)Objc和Swift協(xié)議
}
服務(wù)應(yīng)該中心化注冊還是分布式注冊
中心化注冊是在宿主APP啟動時統(tǒng)一注冊服務(wù)接口的對應(yīng)實現(xiàn)實例,分布式注冊是在組件內(nèi)組件自身進(jìn)行注冊。個人推薦中心化注冊的方式在宿主APP啟動時統(tǒng)一進(jìn)行注冊管理,明確服務(wù)的實現(xiàn)方更清晰,同時避免不同組件包含同一個服務(wù)接口的不同實例導(dǎo)致的沖突。
組件版本兼容
謹(jǐn)慎使用常量、枚舉、宏
因為組件編譯發(fā)布的時候會生成二進(jìn)制庫,編譯器會將依賴的常量、枚舉、宏替換成對應(yīng)的值或代碼,所以當(dāng)后續(xù)這些常量、枚舉、宏發(fā)生變更的時候,已生成的二進(jìn)制庫并不會改變導(dǎo)致打包的時候依然使用的舊值,必須重新發(fā)布使用這些值的組件才行。所以應(yīng)當(dāng)盡量避免修改常量、枚舉、宏值,如果已知后續(xù)可能會變更的情況下應(yīng)避免使用常量、枚舉、宏。
基礎(chǔ)組件API向后兼容
- 對外API需保證向后兼容,使用
添加API的方式擴(kuò)展現(xiàn)有能力,避免對原有API進(jìn)行break change改動或移除 - 使用對象封裝傳遞參數(shù)和回調(diào)參數(shù),避免對
原有API進(jìn)行修改
提示:特別是對于
Objective-C這類動態(tài)調(diào)用的語言來講,打包構(gòu)建時并不能發(fā)現(xiàn)調(diào)用的方法不存在、參數(shù)錯誤這些問題。所以我們應(yīng)當(dāng)盡可能避免現(xiàn)有方法的變更。同時也推薦更多使用Swift編譯器可以發(fā)現(xiàn)這些問題提示編譯錯誤。
減少發(fā)布大版本
以Cocoapods為例,組件發(fā)布大版本會導(dǎo)致依賴此組件的所有組件都必須同時升級到大的版本重新發(fā)布,這樣會給組件使用放帶來極大的更新成本。所以組件應(yīng)該減少發(fā)布大版本,除非必須強(qiáng)制所有組件一定要升級。
優(yōu)先選擇接口服務(wù)減少暴露View類
當(dāng)只關(guān)注API提供的能力并不關(guān)注API提供的形態(tài)時盡可能通過API的方式來暴露能力。因為暴露接口方法相比視圖View,調(diào)用方只需要依賴接口方法相比依賴View類可以更小化的依賴,同時接口對于實現(xiàn)方未來擴(kuò)展能力更靈活。以選擇用戶地址API為例,通常調(diào)用方并不關(guān)注實現(xiàn)方以彈窗的方式還是頁面的方式提供交互能力讓用戶選擇,只關(guān)注用戶最終選擇的地址數(shù)據(jù)。并且調(diào)用方不需要處理彈窗和頁面的展示邏輯使用起來更方便,也便于實現(xiàn)方之后修改交互方式。
使用接口的方法
addressService.chooseAddress { address in
}
使用View的方式
let addressView = AddressView()
addressView.callback = { address in
///
}
addressView.show()
避免使用ObjC Runtime動態(tài)調(diào)用類和方法
第三方庫
第三方庫組件不允許依賴其他組件。
第三方庫組件不允許依賴其他組件。
質(zhì)量保障

雖然前面講到了很多規(guī)范和原則,但是并不能保證我們的這些規(guī)范和原則可以強(qiáng)制執(zhí)行。所以我們需要在組件發(fā)布和應(yīng)用打包階段添加一些卡口安全檢測,及時發(fā)現(xiàn)組件化依賴問題避免帶入線上。
CI檢查
組件發(fā)布
在組件發(fā)布時添加一個安全檢查,避免不符合依賴規(guī)范的組件發(fā)布成功。通常我們可以添加以下依賴檢查規(guī)則:
- 第三方庫不可依賴其他組件
- 基礎(chǔ)組件不可依賴業(yè)務(wù)組件
- 業(yè)務(wù)組件不可直接依賴業(yè)務(wù)組件
- 組件間通常不可相互依賴
- 不允許組件層級間反向依賴
版本集成規(guī)范
集成系統(tǒng)需要將特定需求和組件版本關(guān)聯(lián)到一起,打包時會根據(jù)版本需求自動加入對應(yīng)的組件版本。避免開發(fā)同學(xué)直接修改組件版本引入不應(yīng)該加入到版本的特性。
打包構(gòu)建
在宿主APP打包時,提前檢測出接口服務(wù)存在的問題,避免帶入到線上。通??梢詸z測以下問題:
- 服務(wù)接口對應(yīng)的實現(xiàn)類不存在
- 服務(wù)接口對應(yīng)的實現(xiàn)類沒有實現(xiàn)所有方法
- 使用
ObjC Runtime動態(tài)調(diào)用類和方法
線上異常上報
線上檢查可以幫助我們在灰度發(fā)布的及時發(fā)現(xiàn)問題及時修復(fù),通常可以發(fā)現(xiàn)以下問題:
- 路由跳轉(zhuǎn)對應(yīng)的頁面不存在
- 接口服務(wù)對應(yīng)的實現(xiàn)類不存在
- 接口服務(wù)對應(yīng)的方法不存在
可量化指標(biāo)
我們可以通過一些指標(biāo)來量化整個工程組件化的健康程度,以下列出常見的一些指標(biāo):
基礎(chǔ)組件依賴數(shù)量
組件依賴的所有基礎(chǔ)組件總數(shù),當(dāng)依賴的基礎(chǔ)組件總數(shù)過高時應(yīng)該及時進(jìn)行重構(gòu)。如果大量的業(yè)務(wù)組件都需要依賴非常多的基礎(chǔ)組件,那可能說明基礎(chǔ)組件的依賴關(guān)系出現(xiàn)了很大的問題,這時候需要對基礎(chǔ)組件進(jìn)行優(yōu)化重構(gòu):
- 考慮使用接口服務(wù)對外暴露能力,組件層級需要提升
- 考慮將部分能力拆分出為獨(dú)立的新組件
業(yè)務(wù)服務(wù)依賴數(shù)量
業(yè)務(wù)組件對其他業(yè)務(wù)服務(wù)組件的依賴數(shù)量。當(dāng)業(yè)務(wù)組件依賴了其他業(yè)務(wù)服務(wù)調(diào)用時也會造成隱式的耦合關(guān)系,依賴過多時應(yīng)當(dāng)考慮是否應(yīng)該對外暴露可監(jiān)聽變化的通知訂閱以訂閱觀察的方式替代主動調(diào)用
錯誤依賴關(guān)系數(shù)量
錯誤的依賴關(guān)系應(yīng)該及時優(yōu)化改造。
一些常見的問題
基礎(chǔ)組件應(yīng)該直接暴露還是使用接口暴露
基礎(chǔ)組件應(yīng)該直接使用頭文件API暴露還是使用接口間接暴露有時候很難權(quán)衡,但是可以根據(jù)一些特性來權(quán)衡選擇:
API直接暴露
-
功能單一/依賴少- 一些工具類,例如Foundation -
API復(fù)雜- API非常多如果使用接口需要抽象太多接口,例如網(wǎng)絡(luò)庫、日志 -
UI組件- 需要直接暴露UIView的UI組件,例如UIKit
接口暴露
-
可擴(kuò)展性- 基于接口可以靈活替換不同的實現(xiàn),例如定位能力可以使用系統(tǒng)自帶的API,也可以使用不同地圖廠商的API -
減少依賴引入- 降低使用方的接入成本,提高日常開發(fā)/組件發(fā)布效率 -
可插拔能力- 對應(yīng)的能力可移除,同時也不影響核心業(yè)務(wù) 提示:這些以接口暴露的API還有一個優(yōu)勢是可以抽象成容器化的API,形成統(tǒng)一的標(biāo)準(zhǔn)規(guī)范。使用方調(diào)用同樣的API,不同的APP可以提供不一樣的實現(xiàn)。
小項目是否應(yīng)該做組件化
個人認(rèn)為小項目也可以做組件化,需要關(guān)注的是需要做到什么程度的組件化。通常來講越大型越復(fù)雜的項目組件化拆分的粒度更細(xì)組件數(shù)越多。對于小項目來講雖然早期做組件化的收益并不大,也需要適當(dāng)考慮未來的發(fā)展趨勢預(yù)留一定的空間,同時也需要適當(dāng)考慮模塊間的依賴關(guān)系避免后期拆分模塊時很困難。剛開始做粒度比較粗的組件化,之后在項目發(fā)展中不斷的調(diào)整組件化的粒度。也可以考慮使用類似Monorepo的方式來管理項目,代碼都在一個倉庫中管理,通過文件夾隔離規(guī)范模塊間的依賴。
單一工程如何改造為組件化工程
一般來講我們需要使用循序漸進(jìn)逐步重構(gòu)的策略對原有項目進(jìn)行改造,但是有一些模塊可以優(yōu)先被組件化拆分降低整個組件化的難度:
- 優(yōu)先拆分出最核心的所有業(yè)務(wù)模塊可能都需要使用的組件,這些組件拆分完成以后才能為之后業(yè)務(wù)模塊拆分提供基礎(chǔ)。例如
Foundation、UI組件、網(wǎng)絡(luò)庫、圖片庫、埋點(diǎn)日志等最基礎(chǔ)的組件。 - 優(yōu)先拆分
不被其他組件依賴或被其他組件依賴較少的模塊組件,這些模塊相對比較獨(dú)立拆分起來比較高效并且對現(xiàn)有工程改造較小。例如性能監(jiān)控、微信SDK這類相對獨(dú)立的能力。
組件化帶來的額外成本
組件化架構(gòu)可能會帶來以下這些額外的成本:
- 管理更多的組件
git倉庫 - 每次組件發(fā)布都需要重新編譯/發(fā)布
- 由于組件使用方都是使用相應(yīng)的組件二進(jìn)制庫,所以調(diào)試源碼會變的更困難
- 開發(fā)組件管理平臺,管理組件版本、版本配置表等能力
- 每個組件需要有自己的
Example工程進(jìn)行日常開發(fā)調(diào)試 - 處理可能存在的組件版本不一致導(dǎo)致的依賴沖突、編譯錯誤等問題
- 需求可能會涉及到多組件改動,如何進(jìn)行
Code Review、版本合入檢查
Monorepo
我個人并沒有在實際的項目中使用過Monorepo方式管理項目。Monorepo是將所有組件代碼放在單個git倉庫內(nèi)管理,然后使用文件夾拆分為不同的組件。不同文件夾中的代碼不能直接依賴使用,需要配置本地文件夾的組件依賴關(guān)系,在實現(xiàn)組件化的同時避免拆分太多的git倉庫。不過個人認(rèn)為Monorepo同時也需要解決以下幾個問題:
-
編譯耗時優(yōu)化- 將所有源碼放在單個工程中會導(dǎo)致編譯變慢,所以必須優(yōu)化現(xiàn)有工程編譯流程,降低非必要的重復(fù)編譯耗時。 -
組件版本管理- 在組件化工程中我們可以通過配置組件的特定版本來管理功能是否合入到版本中,但在Monorepo中只能通過分支Merge Request來管理特性是否合入,回滾也會更加繁瑣。 -
高質(zhì)量CI流程- 在單個倉庫中,當(dāng)一個開發(fā)者有倉庫權(quán)限時他就可以修改該倉庫的任意代碼。所以必須完善代碼合入規(guī)范,更高標(biāo)準(zhǔn)的Code Review、集成測試檢查、自動化檢查避免問題代碼帶到線上。
總結(jié)
個人認(rèn)為并不存在一個完美的架構(gòu),我們自身的組織架構(gòu)、業(yè)務(wù)、人員都在變動,架構(gòu)也需要隨著這個過程進(jìn)行適當(dāng)?shù)恼{(diào)整和重構(gòu),最重要的是我們能及時發(fā)現(xiàn)架構(gòu)中存在的問題并且有意愿/能力去調(diào)整避免一直堆積變成更大的技術(shù)債務(wù)。
同時工程架構(gòu)的改變也會一定程度的改變開發(fā)人員的分工,對于大型工程來講組件化的程度更高,每個開發(fā)人員的工作分工會更細(xì)。對于底層基礎(chǔ)組件的開發(fā),需要提供更多高性能/高質(zhì)量的基礎(chǔ)組件讓上層業(yè)務(wù)開發(fā)人員更加效率的支撐業(yè)務(wù),技術(shù)深度也會更加深入。對于上層業(yè)務(wù)開發(fā),更多是使用這些底層基礎(chǔ)組件,同時可能也需要掌握多種跨端UI技術(shù)??焖僦螛I(yè)務(wù),技術(shù)棧會更廣但是不會太深入。