移動端APP組件化架構實踐

作者:何樂樂

前言

對于中大型移動端APP開發(fā)來講,組件化是一種常用的項目架構方式。個人最近幾年在工作項目中也一直使用組件化的方式來開發(fā),在這過程中也積累了一些經(jīng)驗和思考。主要是來自在日常開發(fā)中使用組件化開發(fā)遇到的問題以及和其他開發(fā)同學的交流探討。

本文通過以下問題來介紹組件化這種開發(fā)架構的思想和常見的一些問題:

  • 為什么需要組件化
  • 組件化過程中會遇到的挑戰(zhàn)和選擇
  • 如何維護一個高質量的組件化項目

提示:本文說的組件化工程是指Multirepo使用獨立的git倉庫來管理組件。

組件化可以帶來什么

單一工程架構遇到的問題

在組件化架構之前,傳統(tǒng)使用的工程架構主要是以Monolithic方式的單一工程架構,也就是將所有代碼放在單個代碼倉庫里管理。單一工程架構使用了這么多年為什么突然遇到了問題,這也引入了APP項目開發(fā)的一個大背景,現(xiàn)有中大型APP項目變的越來越復雜:

  • 多APP項目并存 - 集團內部存在多個APP項目,不同APP希望可以復用現(xiàn)有組件能力快速搭建出新的APP
  • 功能增多 - 隨著項目功能越來越多,代碼量增多。同時需要更多的開發(fā)人員參與到項目中,這會增加開發(fā)團隊之間協(xié)作的成本。
  • 多語言/多技術棧 - 引入了更多的新技術,例如使用一種以上的跨平臺UI技術用于快速交付業(yè)務,不同的編程語言、音視頻、跨平臺框架,增加了整個工程的復雜度。

以上這些業(yè)務發(fā)展的訴求就給傳統(tǒng)單一工程架構方式帶來了很多新的技術要求:

工程效率

  • 工程代碼量過大會導致編譯速度緩慢。
  • git工程提交同時可能帶來更多的git提交沖突編譯錯誤。

質量問題

  • 如何將git提交關聯(lián)到對應的功能模塊需求。發(fā)版時進行合規(guī)檢查避免帶入不規(guī)范的代碼,對整個功能模塊回滾的訴求。
  • 如何在單倉庫中管控這么多開發(fā)人員的代碼權限,盡可能避免不安全的提交并且限制改動范圍。

更大范圍的組件復用

  • 基礎組件從支持單個APP復用到支持多個APP復用。
  • 不只是基礎能力組件,對于業(yè)務能力組件也需要支持復用。(例如一個頁面組件同時在多個APP使用)
  • 跨平臺容器需要復用底層組件能力避免重復開發(fā),同時不同跨平臺容器API需要盡量保持統(tǒng)一,底層基礎設施向容器化發(fā)展支持業(yè)務跨APP復用。

跨技術棧通信

  • 由于頁面導航多技術?;旌瞎泊?,頁面路由需要支持跨技術棧。
  • 跨組件通信需要支持跨語言/跨技術棧通信。

更好的解耦

  • 頁面解耦。由于頁面導航棧混合共存,頁面自身不再清晰的知道上游和下游頁面由什么技術棧搭建,所以頁面路由需要做到完全解耦隔離技術棧的具體實現(xiàn)。
  • 業(yè)務組件間維持松耦合關系,可以靈活添加/移除,基于現(xiàn)有組件能力快速搭建出不同的APP。
  • 對于同一個服務或頁面可以插件化方式靈活提供多種不同的實現(xiàn),不同的APP宿主也可以提供不同的實現(xiàn)并且提供A/B能力。
  • 由于包體積限制和不同組件包含相同符號導致的符號沖突問題,在復用組件的時候需要盡可能引入最小依賴原則降低接入成本。

組件化架構的優(yōu)勢

基于以上這些問題,現(xiàn)在的組件化架構希望可以解決這些問題提升整個交付效率和交付質量。

組件化架構通常具備以下優(yōu)點:

  • 代碼復用 - 功能封裝成組件更容易復用到不同的項目中,直接復用可以提高開發(fā)效率。并且每個組件職責單一使用時會帶入最小的依賴。
  • 降低理解復雜度 - 工程拆分為小組件以后,對于組件使用方我們只需要通過組件對外暴露的公開API去使用組件的功能,不需要理解它內部的具體實現(xiàn)。這樣可以幫助我們更容易理解整個大的項目工程。
  • 更好的解耦 - 在傳統(tǒng)單一工程項目中,雖然我們可以使用設計模式或者編碼規(guī)范來約束模塊間的依賴關系,但是由于都存放在單一工程目錄中缺少清晰的模塊邊界依然無法避免不健康的依賴關系。組件化以后可以明確定義需要對外暴露的能力,對于模塊間的依賴關系我們可以進行強約束限制依賴,更好的做到解耦。對一個模塊的添加和移除都會更容易,并且模塊間的依賴關系更加清晰。
  • 隔離技術棧 - 不同的組件可以使用不同的編程語言/技術棧,并且不用擔心會影響到其他組件或主工程。例如在不同的組件內可以自由選擇使用KotlinSwift,可以使用不同的跨平臺框架,只需要通過規(guī)范的方式暴露出頁面路由或者服務方法即可。
  • 獨立開發(fā)/維護/發(fā)布 - 大型項目通常有很多團隊。在傳統(tǒng)單一項目集成打包時可能會遇到代碼提交/分支合并的沖突問題。組件化以后每個團隊負責自己的組件,組件可以獨立開發(fā)/維護/發(fā)布提升開發(fā)效率。
  • 提高編譯/構建速度 - 由于組件會提前編譯發(fā)布成二進制庫進行依賴使用,相比編譯全部源代碼可以節(jié)省大量的編譯耗時。同時在日常組件開發(fā)時只需要編譯少量依賴組件,相比單一工程可以減少大量的編譯耗時和編譯錯誤。
  • 管控代碼權限 - 通過組件化將代碼拆分到不同組件git倉庫中,我們可以更好的管控代碼權限和限制代碼變更范圍。
  • 管理版本變更 - 我們通常會使用CocoaPods/Gradle這類依賴管理工具來管理項目中所有的組件依賴。因為每一個組件都有一個明確的版本,這樣我們可以通過對比APP不同版本打包時的組件依賴表很清晰的識別組件版本特性的變更,避免帶入不合規(guī)的組件版本特性。并且在出現(xiàn)問題時也很方便通過配置表進行回滾撤回。

提示:組件化架構是為了解決單一工程架構開發(fā)中的問題。如果你的項目中也會遇到這些痛點,那可能就需要做組件化。

組件化遇到的挑戰(zhàn)

雖然組件化架構可以帶來這么多收益,但不是只要使用組件化架構就可以解決所有問題。通常來講當我們使用一種新的技術方案解決現(xiàn)有問題的時候也會帶來一些新的問題,組件化架構能帶來多少收益主要取決于整個工程組件化的質量。那在組件化架構中我們如何去評估項目工程的組件化架構質量,我們需要關注哪些問題。對于軟件架構來講,最重要的就是管理組件實體以及組件間的關系。所以對于組件化架構來講主要是關注以下三個問題:

  • 如何劃分組件的粒度、組件職責邊界在哪里?
  • 組件間的依賴關系應該如何管理?
  • 組件間應該使用哪種方式調用和通信?

1. 組件拆分的粒度、組件職責邊界在哪里?

某種程度上組件拆分粒度也是一種平衡的藝術,我們需要在效率質量之間找到一種相對的平衡。組件拆分粒度太粗:導致組件間耦合緊密,并不能利用更好的復用/解耦/提高編譯速度這些優(yōu)勢。組件拆分粒度太細:導致需要維護更多的組件代碼倉庫、功能變更可能涉及多個組件代碼的修改/發(fā)布,這些都會帶來額外的成本,同時組件過多也會導致組件依賴查找過程變的更復雜更慢。

組件的職責也會影響我們對于組件的拆分方式:每個組件的定位是什么,應該包含什么樣的功能,是否可以被復用,添加某個功能的時候應該創(chuàng)建新組件還是添加到現(xiàn)有組件,當組件復雜到一定程度時是否需要拆分出新個組件。

在拆分組件前需要提前去思考這些問題。

2. 組件間的依賴關系應該如何管理?

組件間的依賴方式主要分為直接強耦合依賴間接松耦合依賴強耦合依賴是對依賴的組件直接使用對應的API進行調用,這種調用方式優(yōu)點是簡單直接性能更好,缺點是一種完全耦合的調用方式。(基礎組件通常使用這種方式)。松耦合依賴主要是通過通知URL Scheme、ObjC Runtime、服務接口、事件隊列等通信方式進行間接依賴調用。雖然性能相對差一點,但這是一種相對耦合程度比較低并且靈活的依賴方式。(業(yè)務組件通常使用這種方式)

組件間的依賴關系很重要是因為在長期的項目開發(fā)演化過程中很容易形成一種復雜的網(wǎng)狀依賴關系。雖然看似使用組件化的方式將模塊拆分成不同的組件,但是組件間可能存在很多相互交叉的依賴耦合關系,很多組件都被其他組件直接依賴隱式間接依賴。這樣我們就背離了組件化架構更好的解耦、更好的復用、更快速的開發(fā)/編譯/發(fā)布的初衷。

所以我們需要制定一套規(guī)范去約束和規(guī)范組件間的依賴關系:兩個組件之間是否可以依賴,組件間依賴方向,選擇強耦合依賴還是松耦合依賴。

3. 組件間松耦合依賴關系應該使用哪種方式調用和通信?

松耦合依賴通??梢允褂?code>通知、URL Scheme、ObjC Runtime、服務接口、事件隊列等方式通信進行間接調用,但是使用哪種方式更好業(yè)界也有很多爭論,并且每種方式都有一些優(yōu)缺點。通常在項目中會根據(jù)不同的使用場景至少會選擇2種通信方式。

耦合程度低的方式例如URL Scheme,可以做到完全解耦相對比較靈活。但是無法利用編譯時檢查、無法傳遞復雜對象、調用方/被調用方都需要對參數(shù)做大量的正確性檢查和對齊。同時可能無法檢測對應的調用方法是否存在。

耦合程度高的方式例如服務接口,需要對服務接口方法進行強依賴,但是可以利用編譯時檢查、傳遞復雜對象、并且可以更好的支持Swift特性。

我們需要在解耦程度、容易使用、安全上找到一種合適的方式。

提示:這里的耦合程度高是相對于耦合程度低的方式進行比較,相比直接依賴對應組件依然是一種耦合程度低的依賴關系。

組件化架構實踐規(guī)范和原則

基于以上這些組件化架構的問題,需要一些組件化架構相關的規(guī)范和原則幫助我們做好組件化架構,后面主要會圍繞以下三點進行介紹:

  • 組件拆分原則 - 拆分思想和最佳實踐指導組件拆分
  • 組件間依賴 - 優(yōu)化組件間依賴關系跨組件調用/通信方式的選擇
  • 質量保障 - 避免在持續(xù)的工程演化過程中工程質量逐漸劣化。主要包含安全卡口和CI檢查

工程實例

接下來以一個典型的電商APP架構案例來介紹一個組件化工程。這個案例架構具備之前所說現(xiàn)有中大型APP架構的一些特點,多組件、多技術棧、業(yè)務間需要解耦、復用底層基礎組件?;谶@個案例來介紹上面的三點原則。

組件拆分原則

組件拆分最重要是幫我們梳理出組件職責以及組件職責的邊界。組件劃分也會使用很多通用的設計原則和架構思想。

使用分層思想拆分

通常我們可以首先使用分層架構的思想將所有組件縱向拆分為多層組件,上面層級的組件只能依賴下面層級的組件。一般至少可以劃分為四層組件

  • 基礎層 - 提供核心的與上層業(yè)務無關的基礎能力。可以被上層組件直接依賴使用。
  • 業(yè)務公共層 - 主要包含頁面路由、公共UI組件、跨組件通信以及服務接口,可被上層組件直接依賴使用。
  • 業(yè)務實現(xiàn)層 - 業(yè)務核心實現(xiàn)層,包含原生頁面、跨平臺容器、業(yè)務服務實現(xiàn)。組件間不能直接依賴,只能通過調用頁面路由或跨組件通信組件進行使用。
  • APP宿主層 - 主要包含APP主工程、啟動流程、頁面路由注冊、服務注冊、SDK參數(shù)初始化等組件,用于構建打包生成相應的APP。

劃分層級可以很好的指導我們進行組件拆分。在拆分組件時我們需要先識別它應該在哪一層,它應該以哪種調用方式被其他組件使用,新添加的功能是否會產(chǎn)生反向依賴,幫助我們規(guī)范組件間的依賴關系。同時按層級拆分組件也有利于底層基礎組件的復用。

以下場景使用分層思想就很容易識別:

基礎組件依賴業(yè)務組件

例子:APP內業(yè)務發(fā)起網(wǎng)絡請求通常需要攜帶公共參數(shù)/Cookie。

  • 沒有組件分層約束 - 網(wǎng)絡庫可能會依賴登錄服務獲取用戶信息、依賴定位服務獲取經(jīng)緯度,引入大量的依賴變成業(yè)務組件。
  • 有組件分層約束 - 網(wǎng)絡庫作為一個基礎組件,它不需要關注上層業(yè)務需要攜帶哪些公共業(yè)務參數(shù),同時登錄/定位服務組件在網(wǎng)絡庫上層不能被反向依賴。這時候會考慮單獨創(chuàng)建一個公共參數(shù)管理類,在APP運行時監(jiān)聽各種狀態(tài)的變更并調用網(wǎng)絡庫更新公共參數(shù)/Cookie

業(yè)務組件間依賴方向是否正確

登錄狀態(tài)切換經(jīng)常會涉及到很多業(yè)務邏輯的觸發(fā),例如清空本地用戶緩存、地址緩存、清空購物車數(shù)據(jù)、UI狀態(tài)變更。

  • 沒有組件分層約束 - 可能會在登錄服務內當?shù)卿洜顟B(tài)切換時調用多個業(yè)務邏輯的觸發(fā),導致登錄服務引入多個業(yè)務組件依賴。
  • 有組件分層約束 - 登錄組件只需要在登錄狀態(tài)切換時發(fā)出通知,無需知道登錄狀態(tài)切換會影響哪些業(yè)務。業(yè)務邏輯應該監(jiān)聽登錄狀態(tài)的變更。

識別基礎組件還是業(yè)務組件

雖然很多場景下我們很容易能識別處理出來一個功能應該歸屬于基礎組件還是業(yè)務組件,例如一個UI控件是基礎組件還是業(yè)務組件。但是很多時候邊界又非常的模糊,例如一個添加購物車按鍵應該是一個基礎組件還是業(yè)務組件呢。

  • 基礎組件 - 如果不需要依賴業(yè)務公共層那應當劃分為一個基礎組件。
  • 業(yè)務組件 - 依賴了業(yè)務公共層或者網(wǎng)絡庫,那就應該劃分為一個業(yè)務組件。

分層思想可以很好的幫助我們管理組件間的依賴關系,并且明確每個組件的職責邊界。

基礎/業(yè)務組件拆分原則

劃分基礎/業(yè)務組件主要是為了強制約束組件間的依賴關系。以上面的組件分層架構為例:

  • 基礎組件 - 基礎組件被直接依賴使用,使用方調用基礎組件對外暴露API直接使用。基礎層、業(yè)務公共層都為基礎組件。
  • 業(yè)務組件 - 業(yè)務組件不可被直接依賴使用,只能通過間接通信方式進行使用。APP宿主層業(yè)務實現(xiàn)層都為業(yè)務組件。

提示:這里的業(yè)務組件并不包含業(yè)務UI組件。

基礎組件拆分

基礎組件通常根據(jù)職責單一原則進行拆分比較容易拆分,但是會有一些拆分場景需要考慮:

使用插件組件拆分基礎組件擴展能力

將核心基礎能力和擴展能力拆分到不同的組件。以網(wǎng)絡庫為例,除了提供最核心的接口請求能力,同時可能還包含一些擴展能力例如HTTPDNS、網(wǎng)絡性能檢測、弱網(wǎng)優(yōu)化等能力。但這些擴展能力放在網(wǎng)絡庫組件內部可能會導致以下問題:

  • 擴展能力會使組件自身代碼變的更加復雜。
  • 使用方不一定會使用所有這些擴展能力違反了最小依賴原則。帶來更多的包體積,引入更多的組件依賴,增加模塊間的耦合度。
  • 相關的擴展能力不支持靈活的替換/插拔。

所以這種場景我們可以考慮根據(jù)實際情況將擴展能力拆分到相應的插件組件,使用方需要時再依賴引入對應插件組件。

業(yè)務組件拆分

業(yè)務頁面拆分方式

針對業(yè)務頁面可以使用技術棧業(yè)務域、頁面粒度三種方式進行更細粒度的劃分,通常至少要拆分到技術棧、業(yè)務域這一層級,頁面粒度拆分根據(jù)具體頁面復雜度和復用訴求。

  • 基于技術棧進行拆分 - 不同的技術棧需要拆分到不同的組件進行管理。
  • 基于業(yè)務域進行拆分 - 將同一個業(yè)務域的所有頁面拆分一個組件,避免不同業(yè)務域之間形成強耦合依賴關系,同一個業(yè)務域通常會有更多復用和通信的場景也方便開發(fā)。例如訂單詳情和訂單列表可放置在一起管理。
  • 基于頁面粒度進行拆分 - 單個頁面復雜度過高或需要被單獨復用時需要拆分到一個單個組件管理。

提示:放置在單一組件內的多個頁面之間也應適當降低耦合程度。

第三方庫

第三方庫應拆分單獨組件管理

第三方庫應使用獨立的組件進行管理,一方面有利于組件復用同時避免多個重復第三方庫導致符號沖突,另一方面有利于后續(xù)升級維護。

一些提示

減少使用通用聚合公共組件

為了避免拆分過多的組件,我們通常會創(chuàng)建聚合組件將一些代碼量不多/功能相似的類放到同一個組件內,例如Foundation組件、UI組件。但是很多時候會存在濫用的場景,應當警惕這類公共聚合組件。下面是一些公共聚合組件容易濫用的場景:

  • 添加一個新功能不知道應當加在哪里時,就加到公共聚合組件內,時間久了以后公共組件依賴特別多。
  • 公共組件添加了一個非常復雜的能力,導致復雜度變高或者引入大量依賴
  • 太多能力聚合到一起。例如將網(wǎng)絡庫、圖片庫這些能力放在同一個組件內
  • 基礎/業(yè)務UI組件沒有拆分?;AUI組件通常只提供最基礎的UI和非常輕量的邏輯,業(yè)務組件通常會充當基礎UI組件的數(shù)據(jù)源以及業(yè)務邏輯。

但是也不能完全避免使用聚合公共組件,不然會導致產(chǎn)生更多的小組件增加維護成本。但是我們將一個能力添加到公共聚合組件時可以根據(jù)以下幾個條件來權衡:

  • 是否會引入大量新的依賴
  • 功能復雜度、代碼數(shù)量,太復雜的不應該添加到公共組件
  • 能力是否需要被單獨復用,需要單獨復用就不應該添加到公共組件

第三方庫考慮不直接對外暴露使用

當存在以下情況時可考慮對第三方庫進行適當?shù)姆庋b避免直接暴露第三方庫:

  • 使用方通常只需要使用少量API,第三方庫會對外暴露大量API增加使用難度,同時可能導致一些安全問題
  • 對外隱藏具體實現(xiàn),方便后續(xù)更換其他第三方庫、自實現(xiàn)、第三方庫發(fā)生Break Change變更時升級更容易
  • 需要封裝擴展一些能力讓使用方使用起來更容易

以網(wǎng)絡庫為例:
1.通常需要對接公司內部的API網(wǎng)關能力所以需要適當做一些封裝,例如簽名或者加密策略。
2.使用方通常只需要用到一個通用的請求方法無需對外暴露太多API。
3.為了安全通常需要對業(yè)務方隱藏一些方法避免錯誤調用,例如全局Cookie修改等能力。
4.對外隱藏具體第三方庫可以方便變更。

第三方庫盡可能避免直接修改源碼

第三方庫組件盡可能不要直接修改源碼,除修復Bug/Crash之外盡可能避免帶入其他功能代碼導致后面更新困難。需要添加功能時可以通過在其他組件內使用第三方庫對外暴露的API進行能力擴展。

組件間依賴關系

業(yè)務組件間通信方式選擇

松耦合通信方式對比

基于以上表格中各種方案的優(yōu)缺點,個人推薦使用URL Scheme協(xié)議作為頁面路由通信方式,使用服務接口提供業(yè)務功能服務。通知訂閱場景可使用通知RxSwift方式提供一對多的訂閱能力。

服務接口

服務接口對應的實現(xiàn)和頁面是否需要拆分

以購物車服務為例,購物車接口服務提供了添加購物車的能力。加車服務具體的實現(xiàn)應該放在購物車頁面組件內還是獨立出來放置在單獨的組件。將購物車服務實現(xiàn)和購物車頁面拆分的優(yōu)點是購物車服務和購物車頁面更好的解耦,都能單獨支持復用。缺點是開發(fā)效率降低,修改購物車功能時可能會涉及到同時修改購物車服務組件和購物車頁面組件。

所以在需要單獨復用服務頁面的場景時可考慮分別拆分出單個組件(例如購物車服務作為一種通用能力提供給上層跨平臺容器能力)。但即使在同一個組件內也建議對服務和頁面使用分層設計的方式進行解耦。

服務接口是否需要拆分

一般項目可能至少會有10+個服務接口,這些服務接口應該統(tǒng)一存放在單個組件還是每個接口對應一個組件。

  • 統(tǒng)一存放:優(yōu)點是一起管理更快捷方便。缺點是所有接口對應一個組件版本,不能支持單一接口使用不同版本,不利于需要跨APP復用的項目。并且使用方可能會引入大量無用的接口依賴。
  • 分開存放:優(yōu)點是每個接口可使用不同的版本并且使用方只需要依賴特定的接口。缺點是會產(chǎn)生更多的組件倉庫,組件數(shù)量也會增加依賴查找的耗時。 所以大型項目選擇分開存放的方式管理接口相對更合適一點。也可以考慮將大部分最核心的服務接口放置到一起管理。

支持Swift的服務接口實現(xiàn)推薦

使用Swift實現(xiàn)傳統(tǒng)的服務接口模式通常會遇到以下兩個問題:

  • 接口需要同時支持Objective-CSwift調用,同時希望使用Swift特性設計API。如何實現(xiàn)Objective-CSwift協(xié)議可以復用一個實例
  • Swift對于動態(tài)性支持比較弱,純Swift類無法支持運行時動態(tài)創(chuàng)建只能在注冊時創(chuàng)建實例

基于以上問題,個人推薦使用下面的方式實現(xiàn)接口服務模式:

  • 使用Objective-C協(xié)議提供最基礎的服務能力,之后創(chuàng)建Swift協(xié)議擴展提供部分Swift特性的API
  • 接口實現(xiàn)類繼承NSObject支持運行時動態(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é)議
}

服務應該中心化注冊還是分布式注冊

中心化注冊是在宿主APP啟動時統(tǒng)一注冊服務接口的對應實現(xiàn)實例,分布式注冊是在組件內組件自身進行注冊。個人推薦中心化注冊的方式在宿主APP啟動時統(tǒng)一進行注冊管理,明確服務的實現(xiàn)方更清晰,同時避免不同組件包含同一個服務接口的不同實例導致的沖突。

組件版本兼容

謹慎使用常量、枚舉、宏

因為組件編譯發(fā)布的時候會生成二進制庫,編譯器會將依賴的常量、枚舉、宏替換成對應的值或代碼,所以當后續(xù)這些常量、枚舉、宏發(fā)生變更的時候,已生成的二進制庫并不會改變導致打包的時候依然使用的舊值,必須重新發(fā)布使用這些值的組件才行。所以應當盡量避免修改常量、枚舉、宏值,如果已知后續(xù)可能會變更的情況下應避免使用常量、枚舉、宏

基礎組件API向后兼容

  • 對外API需保證向后兼容,使用添加API的方式擴展現(xiàn)有能力,避免對原有API進行break change改動或移除
  • 使用對象封裝傳遞參數(shù)和回調參數(shù),避免對原有API進行修改

提示:特別是對于Objective-C這類動態(tài)調用的語言來講,打包構建時并不能發(fā)現(xiàn)調用的方法不存在、參數(shù)錯誤這些問題。所以我們應當盡可能避免現(xiàn)有方法的變更。同時也推薦更多使用Swift編譯器可以發(fā)現(xiàn)這些問題提示編譯錯誤。

減少發(fā)布大版本

Cocoapods為例,組件發(fā)布大版本會導致依賴此組件的所有組件都必須同時升級到大的版本重新發(fā)布,這樣會給組件使用放帶來極大的更新成本。所以組件應該減少發(fā)布大版本,除非必須強制所有組件一定要升級。

優(yōu)先選擇接口服務減少暴露View類

當只關注API提供的能力并不關注API提供的形態(tài)時盡可能通過API的方式來暴露能力。因為暴露接口方法相比視圖View,調用方只需要依賴接口方法相比依賴View類可以更小化的依賴,同時接口對于實現(xiàn)方未來擴展能力更靈活。以選擇用戶地址API為例,通常調用方并不關注實現(xiàn)方以彈窗的方式還是頁面的方式提供交互能力讓用戶選擇,只關注用戶最終選擇的地址數(shù)據(jù)。并且調用方不需要處理彈窗和頁面的展示邏輯使用起來更方便,也便于實現(xiàn)方之后修改交互方式。

使用接口的方法
addressService.chooseAddress { address in

}
使用View的方式
let addressView = AddressView()
addressView.callback = { address in
    ///
}
addressView.show()

避免使用ObjC Runtime動態(tài)調用類和方法

第三方庫

第三方庫組件不允許依賴其他組件。

第三方庫組件不允許依賴其他組件。

質量保障

雖然前面講到了很多規(guī)范和原則,但是并不能保證我們的這些規(guī)范和原則可以強制執(zhí)行。所以我們需要在組件發(fā)布和應用打包階段添加一些卡口安全檢測,及時發(fā)現(xiàn)組件化依賴問題避免帶入線上。

CI檢查

組件發(fā)布

在組件發(fā)布時添加一個安全檢查,避免不符合依賴規(guī)范的組件發(fā)布成功。通常我們可以添加以下依賴檢查規(guī)則:

  • 第三方庫不可依賴其他組件
  • 基礎組件不可依賴業(yè)務組件
  • 業(yè)務組件不可直接依賴業(yè)務組件
  • 組件間通常不可相互依賴
  • 不允許組件層級間反向依賴

版本集成規(guī)范

集成系統(tǒng)需要將特定需求和組件版本關聯(lián)到一起,打包時會根據(jù)版本需求自動加入對應的組件版本。避免開發(fā)同學直接修改組件版本引入不應該加入到版本的特性。

打包構建

在宿主APP打包時,提前檢測出接口服務存在的問題,避免帶入到線上。通??梢詸z測以下問題:

  • 服務接口對應的實現(xiàn)類不存在
  • 服務接口對應的實現(xiàn)類沒有實現(xiàn)所有方法
  • 使用ObjC Runtime動態(tài)調用類和方法

線上異常上報

線上檢查可以幫助我們在灰度發(fā)布的及時發(fā)現(xiàn)問題及時修復,通常可以發(fā)現(xiàn)以下問題:

  • 路由跳轉對應的頁面不存在
  • 接口服務對應的實現(xiàn)類不存在
  • 接口服務對應的方法不存在

可量化指標

我們可以通過一些指標來量化整個工程組件化的健康程度,以下列出常見的一些指標:

基礎組件依賴數(shù)量

組件依賴的所有基礎組件總數(shù),當依賴的基礎組件總數(shù)過高時應該及時進行重構。如果大量的業(yè)務組件都需要依賴非常多的基礎組件,那可能說明基礎組件的依賴關系出現(xiàn)了很大的問題,這時候需要對基礎組件進行優(yōu)化重構:

  • 考慮使用接口服務對外暴露能力,組件層級需要提升
  • 考慮將部分能力拆分出為獨立的新組件

業(yè)務服務依賴數(shù)量

業(yè)務組件對其他業(yè)務服務組件的依賴數(shù)量。當業(yè)務組件依賴了其他業(yè)務服務調用時也會造成隱式的耦合關系,依賴過多時應當考慮是否應該對外暴露可監(jiān)聽變化的通知訂閱以訂閱觀察的方式替代主動調用

錯誤依賴關系數(shù)量

錯誤的依賴關系應該及時優(yōu)化改造。

一些常見的問題

基礎組件應該直接暴露還是使用接口暴露

基礎組件應該直接使用頭文件API暴露還是使用接口間接暴露有時候很難權衡,但是可以根據(jù)一些特性來權衡選擇:

API直接暴露
  • 功能單一/依賴少 - 一些工具類,例如Foundation
  • API復雜 - API非常多如果使用接口需要抽象太多接口,例如網(wǎng)絡庫、日志
  • UI組件 - 需要直接暴露UIViewUI組件,例如UIKit
接口暴露
  • 可擴展性 - 基于接口可以靈活替換不同的實現(xiàn),例如定位能力可以使用系統(tǒng)自帶的API,也可以使用不同地圖廠商的API
  • 減少依賴引入 - 降低使用方的接入成本,提高日常開發(fā)/組件發(fā)布效率
  • 可插拔能力 - 對應的能力可移除,同時也不影響核心業(yè)務 提示:這些以接口暴露的API還有一個優(yōu)勢是可以抽象成容器化的API,形成統(tǒng)一的標準規(guī)范。使用方調用同樣的API,不同的APP可以提供不一樣的實現(xiàn)。

小項目是否應該做組件化

個人認為小項目也可以做組件化,需要關注的是需要做到什么程度的組件化。通常來講越大型越復雜的項目組件化拆分的粒度更細組件數(shù)越多。對于小項目來講雖然早期做組件化的收益并不大,也需要適當考慮未來的發(fā)展趨勢預留一定的空間,同時也需要適當考慮模塊間的依賴關系避免后期拆分模塊時很困難。剛開始做粒度比較粗的組件化,之后在項目發(fā)展中不斷的調整組件化的粒度。也可以考慮使用類似Monorepo的方式來管理項目,代碼都在一個倉庫中管理,通過文件夾隔離規(guī)范模塊間的依賴。

單一工程如何改造為組件化工程

一般來講我們需要使用循序漸進逐步重構的策略對原有項目進行改造,但是有一些模塊可以優(yōu)先被組件化拆分降低整個組件化的難度:

  • 優(yōu)先拆分出最核心的所有業(yè)務模塊可能都需要使用的組件,這些組件拆分完成以后才能為之后業(yè)務模塊拆分提供基礎。例如FoundationUI組件、網(wǎng)絡庫圖片庫、埋點日志等最基礎的組件。
  • 優(yōu)先拆分不被其他組件依賴被其他組件依賴較少的模塊組件,這些模塊相對比較獨立拆分起來比較高效并且對現(xiàn)有工程改造較小。例如性能監(jiān)控微信SDK這類相對獨立的能力。

組件化帶來的額外成本

組件化架構可能會帶來以下這些額外的成本:

  • 管理更多的組件git倉庫
  • 每次組件發(fā)布都需要重新編譯/發(fā)布
  • 由于組件使用方都是使用相應的組件二進制庫,所以調試源碼會變的更困難
  • 開發(fā)組件管理平臺,管理組件版本、版本配置表等能力
  • 每個組件需要有自己的Example工程進行日常開發(fā)調試
  • 處理可能存在的組件版本不一致導致的依賴沖突、編譯錯誤等問題
  • 需求可能會涉及到多組件改動,如何進行Code Review版本合入檢查

Monorepo

我個人并沒有在實際的項目中使用過Monorepo方式管理項目。Monorepo是將所有組件代碼放在單個git倉庫內管理,然后使用文件夾拆分為不同的組件。不同文件夾中的代碼不能直接依賴使用,需要配置本地文件夾的組件依賴關系,在實現(xiàn)組件化的同時避免拆分太多的git倉庫。不過個人認為Monorepo同時也需要解決以下幾個問題:

  • 編譯耗時優(yōu)化 - 將所有源碼放在單個工程中會導致編譯變慢,所以必須優(yōu)化現(xiàn)有工程編譯流程,降低非必要的重復編譯耗時。
  • 組件版本管理 - 在組件化工程中我們可以通過配置組件的特定版本來管理功能是否合入到版本中,但在Monorepo中只能通過分支Merge Request來管理特性是否合入,回滾也會更加繁瑣。
  • 高質量CI流程 - 在單個倉庫中,當一個開發(fā)者有倉庫權限時他就可以修改該倉庫的任意代碼。所以必須完善代碼合入規(guī)范,更高標準的Code Review、集成測試檢查、自動化檢查避免問題代碼帶到線上。

總結

個人認為并不存在一個完美的架構,我們自身的組織架構、業(yè)務、人員都在變動,架構也需要隨著這個過程進行適當?shù)恼{整和重構,最重要的是我們能及時發(fā)現(xiàn)架構中存在的問題并且有意愿/能力去調整避免一直堆積變成更大的技術債務。

同時工程架構的改變也會一定程度的改變開發(fā)人員的分工,對于大型工程來講組件化的程度更高,每個開發(fā)人員的工作分工會更細。對于底層基礎組件的開發(fā),需要提供更多高性能/高質量的基礎組件讓上層業(yè)務開發(fā)人員更加效率的支撐業(yè)務,技術深度也會更加深入。對于上層業(yè)務開發(fā),更多是使用這些底層基礎組件,同時可能也需要掌握多種跨端UI技術??焖僦螛I(yè)務,技術棧會更廣但是不會太深入。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容