本文作者: 伯樂線上 - 劉小壯 。未經作者許可,禁止轉載!
歡迎加入伯樂線上 專欄作者。
前段時間公司項目打算重構,準确來說應該是按之前的産品邏輯重寫一個項目?。在重構項目之前涉及到架構選型的問題,我群組裡小夥伴一起研究了一下元件化架構,打算将項目重構為元件化架構。當然不是直接拿來照搬,還是要根據公司具體的業務需求設計架構。
在學習元件化架構的過程中,從很多高品質的部落格中學到不少東西,例如蘑菇街李忠、casatwy、bang的部落格。在學習過程中也遇到一些問題,在微網誌和QQ上和一些做
iOS
的朋友進行了交流,非常感謝這些朋友的幫助。
本篇文章主要針對于之前蘑菇街提出的元件化方案,以及casatwy提出的元件化方案進行分析,後面還會簡單提到滴滴、淘寶、微信的元件化架構,最後會簡單說一下我公司設計的元件化架構。
元件化架構的由來
随着移動網際網路的不斷發展,很多程式代碼量和業務越來越多,現有架構已經不适合公司業務的發展速度了,很多都面臨着重構的問題。
在公司項目開發中,如果項目比較小,普通的
單工程+MVC架構
就可以滿足大多數需求了。但是像淘寶、蘑菇街、微信這樣的大型項目,原有的單工程架構就不足以滿足架構需求了。
就拿淘寶來說,淘寶在13年開啟的
“All in 無線”
戰略中,就将阿裡系大多數業務都加入到手機淘寶中,使用戶端出現了業務的爆發。在這種情況下,單工程架構則已經遠遠不能滿足現有業務需求了。是以在這種情況下,淘寶在13年開啟了插件化架構的重構,後來在14年迎來了手機淘寶有史以來最大規模的重構,将其徹底重構為元件化架構。
蘑菇街的元件化架構
原因
在一個項目越來越大,開發人員越來越多的情況下,項目會遇到很多問題。
- 業務子產品間劃分不清晰,子產品之間耦合度很大,非常難維護。
- 所有子產品代碼都編寫在一個項目中,測試某個子產品或功能,需要編譯運作整個項目。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcuYGZ4UWY0gDMiNTOhVmZhVmN5UDOxUTMwITN5kzNlZDOvwVOw8CX2EDMy8CXt92Yu4Wd5lWYwVnLwImLy4GZjJmavw1LcpDc0RHaiojIsJye.png)
耦合嚴重的工程
為了解決上面的問題,可以考慮加一個中間層來協調子產品間的調用,所有的子產品間的調用都會經過中間層中轉。(注意看兩張圖的箭頭方向)
添加中間層
但是發現增加這個中間層後,耦合還是存在的。中間層對被調用子產品存在耦合,其他子產品也需要耦合中間層才能發起調用。這樣還是存在之前的互相耦合的問題,而且本質上比之前更麻煩了。
大體結構
是以應該做的是,隻讓其他子產品對中間層産生耦合關系,中間層不對其他子產品發生耦合。
對于這個問題,可以采用元件化的架構,将每個子產品作為一個元件。并且建立一個主項目,這個主項目負責內建所有元件。這樣帶來的好處是很多的:
- 業務劃分更佳清晰,新人接手更佳容易,可以按元件配置設定開發任務。
- 項目可維護性更強,提高開發效率。
- 更好排查問題,某個元件出現問題,直接對元件進行處理。
- 開發測試過程中,可以隻編譯自己那部分代碼,不需要編譯整個項目代碼。
元件化結構
進行元件化開發後,可以把每個元件當做一個獨立的app,每個元件甚至可以采取不同的架構,例如分别使用
MVVM
、
MVC
、
MVCS
等架構。
MGJRouter方案
蘑菇街通過
MGJRouter
實作中間層,通過
MGJRouter
進行元件間的消息轉發,從名字上來說更像是路由器。實作方式大緻是,在提供服務的元件中提前注冊
block
,然後在調用方元件中通過
URL
調用
block
,下面是調用方式。
架構設計
MGJRouter元件化架構
MGJRouter
是一個單例對象,在其内部維護着一個
“URL -> block”
格式的系統資料庫,通過這個系統資料庫來儲存服務方注冊的
block
,以及使調用方可以通過
URL
映射出
block
,并通過
MGJRouter
對服務方發起調用。
在服務方元件中都對外提供一個接口類,在接口類内部實作
block
的注冊工作,以及
block
對外提供服務的代碼實作。每一個
block
都對應着一個
URL
,調用方可以通過
URL
對
block
發起調用。
在程式開始運作時,需要将所有服務方的接口類執行個體化,以完成這個注冊工作,使
MGJRouter
中所有服務方的
block
可以正常提供服務。在這個服務注冊完成後,就可以被調用方調起并提供服務。
蘑菇街項目使用
git
作為版本控制工具,将每個元件都當做一個獨立工程,并建立主項目來內建所有元件。內建方式是在主項目中通過
CocoaPods
來內建,将所有元件當做二方庫內建到項目中。詳細的內建技術點在下面
“标準元件化架構設計”
章節中會講到。
MGJRouter調用
代碼模拟對詳情頁的注冊、調用,在調用過程中傳遞
id
參數。下面是注冊的示例代碼:
1 2 3 4 | [MGJRouter registerURLPattern:@"mgj://detail?id=id" toHandler:^(NSDictionary *routerParameters) { // 下面可以在拿到參數後,為其他元件提供對應的服務 NSString uid = routerParameters[@"id"]; }]; |
通過
openURL:
方法傳入的
URL
參數,對詳情頁已經注冊的
block
方法發起調用。調用方式類似于
GET
請求,
URL
位址後面拼接參數。
1 | [MGJRouter openURL:@"mgj://detail?id=404"]; |
也可以通過字典方式傳參,
MGJRouter
提供了帶有字典參數的方法,這樣就可以傳遞非字元串之外的其他類型參數。
1 | [MGJRouter openURL:@"mgj://detail?" withParam:@{@"id" : @"404"}]; |
元件間傳值
有的時候元件間調用過程中,需要服務方在完成調用後傳回相應的參數。蘑菇街提供了另外的方法,專門來完成這個操作。
1 2 3 | [MGJRouter registerURLPattern:@"mgj://cart/ordercount" toObjectHandler:^id(NSDictionary *routerParamters){ return @42; }]; |
通過下面的方式發起調用,并擷取服務方傳回的傳回值,要做的就是傳遞正确的
URL
和參數即可。
1 | NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"]; |
短鍊管理
這時候會發現一個問題,在蘑菇街元件化架構中,存在了很多寫死的URL和參數。在代碼實作過程中
URL
編寫出錯會導緻調用失敗,而且參數是一個字典類型,調用方不知道服務方需要哪些參數,這些都是個問題。
對于這些資料的管理,蘑菇街開發了一個
web
頁面,這個
web
頁面統一來管理所有的
URL
和參數,
Android
和
iOS
都使用這一套
URL
,可以保持統一性。
基礎元件
在項目中存在很多公共部分的東西,例如封裝的網絡請求、緩存、資料處理等功能,以及項目中所用到的資源檔案。
蘑菇街将這些部分也當做元件,劃分為基礎元件,位于業務元件下層。所有業務元件都使用同一個基礎元件,也可以保證公共部分的統一性。
Protocol方案
整體架構
Protocol方案的中間件
為了解決
MGJRouter
方案中
URL
寫死,以及字典參數類型不明确等問題,蘑菇街在原有元件化方案的基礎上推出了
Protocol
方案。
Protocol
方案由兩部分組成,進行元件間通信的
ModuleManager
類以及
MGJComponentProtocol
協定類。
通過中間件
ModuleManager
進行消息的調用轉發,在
ModuleManager
内部維護一張映射表,映射表由之前的
"URL -> block"
變成
"Protocol -> Class"
。
在中間件中建立
MGJComponentProtocol
檔案,服務方元件将可以用來調用的方法都定義在
Protocol
中,将所有服務方的
Protocol
都分别定義到
MGJComponentProtocol
檔案中,如果協定比較多也可以分開幾個檔案定義。這樣所有調用方依然是隻依賴中間件,不需要依賴除中間件之外的其他元件。
Protocol
方案中每個元件也需要一個“接口類”,此類負責實作目前元件對應的協定方法,也就是對外提供服務的實作。在程式開始運作時将自身的
Class
注冊到
ModuleManager
中,并将
Protocol
反射出字元串當做
key
。這個注冊過程和
MGJRouter
是類似的,都需要提前注冊服務。
示例代碼
建立
MGJUserImpl
類當做
User
子產品的服務類,并在
MGJComponentProtocol.h
中定義
MGJUserProtocol
協定,由
MGJUserImpl
類實作協定中定義的方法,完成對外提供服務的過程。下面是協定定義:
1 2 3 | @protocol MGJUserProtocol - (NSString *)getUserName; @end |
Class
遵守協定并實作定義的方法,外界通過
Protocol
擷取的
Class
執行個體化為對象,調用服務方實作的協定方法。
ModuleManager
的協定注冊方法,注冊時将
Protocol
反射為字元串當做存儲的
key
,将實作協定的
Class
當做值存儲。通過
Protocol
取
Class
的時候,就是通過
Protocol
從
ModuleManager
中将
Class
映射出來。
1 | [ModuleManager registerClass:MGJUserImpl forProtocol:@protocol(MGJUserProtocol)]; |
調用時通過
Protocol
從
ModuleManager
中映射出注冊的
Class
,将擷取到的
Class
執行個體化,并調用
Class
實作的協定方法完成服務調用。
1 2 3 | Class cls = [[ModuleManager sharedInstance] classForProtocol:@protocol(MGJUserProtocol)]; id userComponent = [[cls alloc] init]; NSString *userName = [userComponent getUserName]; |
整體調用流程
蘑菇街是
OpenURL
和
Protocol
混用的方式,兩種實作的調用方式不同,但大體調用邏輯和實作思路類似,是以下面的調用流程二者差不多。在
OpenURL
不能滿足需求或調用不友善時,就可以通過
Protocol
的方式調用。
- 在進入程式後,先使用
對服務方元件進行注冊。每個MGJRouter
對應一個URL
的實作,block
中的代碼就是服務方對外提供的服務,調用方可以通過block
調用這個服務。URL
- 調用方通過
調用MGJRouter
方法,并将被調用代碼對應的openURL:
傳入,URL
會根據MGJRouter
查找對應的URL
實作,進而調用服務方元件的代碼進行通信。block
- 調用和注冊
時,block
有一個字典用來傳遞參數。這樣的優勢就是參數類型和數量理論上是不受限制的,但是需要很多寫死的block
名在項目中。key
記憶體管理
蘑菇街元件化方案有兩種,
Protocol
和
MGJRouter
的方式,但都需要進行
register
操作。
Protocol
注冊的是
Class
,
MGJRouter
注冊的是
Block
,系統資料庫是一個
NSMutableDictionary
類型的字典,而字典的擁有者又是一個單例對象,這樣會造成記憶體的常駐。
下面是對兩種實作方式記憶體消耗的分析:
- 首先說一下
實作方式可能導緻的記憶體問題,block
block
如果使用不當,很容易造成循環引用的問題。
經過暴力測試,證明并不會導緻記憶體問題。被儲存在字典中是一個
對象,而block
對象本身并不會占用多少記憶體。在調用block
後會對block
體中的方法進行執行,執行完成後block
block
體中的對象釋放。
而
自身的實作隻是一個結構體,也就相當于字典中存放的是很多結構體,是以記憶體的占用并不是很大。block
- 對于協定這種實作方式,和
記憶體常駐方式差不多。隻是将存儲的block
對象換成block
對象,如果不是已經執行個體化的對象,記憶體占用還是比較小的。Class
casatwy元件化方案
整體架構
casatwy元件化方案分為兩種調用方式,遠端調用和本地調用,對于兩個不同的調用方式分别對應兩個接口。
- 遠端調用通過
代理方法傳遞到目前應用後,調用遠端接口并在内部做一些處理,處理完成後會在遠端接口内部調用本地接口,以實作本地調用為遠端調用服務。AppDelegate
- 本地調用由
方法負責,但調用方一般不直接調用performTarget:action:params:
方法。performTarget:
會對外提供明确參數和方法名的方法,在方法内部調用CTMediator
方法和參數的轉換。performTarget:
casatwy提出的元件化架構
架構設計思路
casatwy是通過
CTMediator
類實作元件化的,在此類中對外提供明确參數類型的接口,接口内部通過
performTarget
方法調用服務方元件的
Target
、
Action
。由于
CTMediator
類的調用是通過
runtime
主動發現服務的,是以服務方對此類是完全解耦的。
但如果
CTMediator
類對外提供的方法都放在此類中,将會對
CTMediator
造成極大的負擔和代碼量。解決方法就是對每個服務方元件建立一個
CTMediator
的
Category
,并将對服務方的
performTarget
調用放在對應的
Category
中,這些
Category
都屬于
CTMediator
中間件,進而實作了感官上的接口分離。
casatwy元件化實作細節
對于服務方的元件來說,每個元件都提供一個或多個
Target
類,在
Target
類中聲明
Action
方法。
Target
類是目前元件對外提供的一個“服務類”,
Target
将目前元件中所有的服務都定義在裡面,
CTMediator
通過
runtime
主動發現服務。
在
Target
中的所有
Action
方法,都隻有一個字典參數,是以可以傳遞的參數很靈活,這也是casatwy提出的去
Model
化的概念。在
Action
的方法實作中,對傳進來的字典參數進行解析,再調用元件内部的類和方法。
架構分析
casatwy為我們提供了一個Demo,通過這個
Demo
可以很好的了解casatwy的設計思路,下面按照我的了解講解一下這個
Demo
。
檔案目錄
打開
Demo
後可以看到檔案目錄非常清楚,在上圖中用藍框框出來的就是中間件部分,紅框框出來的就是業務元件部分。我對每個檔案夾做了一個簡單的注釋,包含了其在架構中的職責。
在
CTMediator
中定義遠端調用和本地調用的兩個方法,其他業務相關的調用由
Category
完成。
1 2 3 4 | // 遠端App調用入口 - (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion; // 本地元件調用入口 - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params; |
在
CTMediator
中定義的
ModuleA
的
Category
,對外提供了一個擷取控制器并跳轉的功能,下面是代碼實作。由于casatwy的方案中使用
performTarget
的方式進行調用,是以涉及到很多寫死字元串的問題,casatwy采取定義常量字元串來解決這個問題,這樣管理也更友善。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #import "CTMediator+CTMediatorModuleAActions.h" NSString * const kCTMediatorTargetA = @"A"; NSString * const kCTMediatorActionNativFetchDetailViewController = @"nativeFetchDetailViewController"; @implementation CTMediator (CTMediatorModuleAActions) - (UIViewController *)CTMediator_viewControllerForDetail { UIViewController *viewController = [self performTarget:kCTMediatorTargetA action:kCTMediatorActionNativFetchDetailViewController params:@{@"key":@"value"}]; if ([viewController isKindOfClass:[UIViewController class]]) { // view controller 傳遞出去之後,可以由外界選擇是push還是present return viewController; } else { // 這裡處理異常場景,具體如何處理取決于産品 return [[UIViewController alloc] init]; } } |
下面是
ModuleA
元件中提供的服務,被定義在
Target_A
類中,這些服務可以被
CTMediator
通過
runtime
的方式調用,這個過程就叫做發現服務。
我們發現,在這個方法中其實做了參數處理和内部調用的功能,這樣就可以保證元件内部的業務不受外部影響,對内部業務沒有侵入性。
1 2 3 4 5 6 | - (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params { // 對傳過來的字典參數進行解析,并調用ModuleA内部的代碼 DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init]; viewController.valueLabel.text = params[@"key"]; return viewController; } |
命名規範
在大型項目中代碼量比較大,需要避免命名沖突的問題。對于這個問題casatwy采取的是加字首的方式,從casatwy的
Demo
中也可以看出,其元件
ModuleA
的
Target
命名為
Target_A
,被調用的
Action
命名為
Action_nativeFetchDetailViewController:
。
casatwy将類和方法的命名,都統一按照其功能做區分當做字首,這樣很好的将元件相關群組件内部代碼進行了劃分。
标準元件化架構設計
這個章節叫做“标準元件化架構設計”,對于項目架構來說并沒有絕對意義的标準之說。這裡說到的“标準元件化架構設計”隻是因為采取這樣的方式的人比較多,且這種方式相比而言較合理。
在上面文章中提到了casatwy方案的
CTMediator
,蘑菇街方案的
MGJRouter
和
ModuleManager
,下面統稱為中間件。
整體架構
元件化架構中,首先有一個主工程,主工程負責內建所有元件。每個元件都是一個單獨的工程,建立不同的
git
私有倉庫來管理,每個元件都有對應的開發人員負責開發。開發人員隻需要關注與其相關元件的代碼,其他業務代碼和其無關,來新人也好上手。
元件的劃分需要注意元件粒度,粒度根據業務可大可小。元件劃分後屬于業務元件,對于一些多個元件共同的東西,例如網絡、資料庫之類的,應該劃分到單獨的元件或基礎元件中。對于圖檔或配置表這樣的資源檔案,應該再單獨劃分一個資源元件,這樣避免資源的重複性。
服務方元件對外提供服務,由中間件調用或發現服務,服務對目前元件無侵入性,隻負責對傳遞過來的資料進行解析群組件内調用的功能。需要被其他元件調用的元件都是服務方,服務方也可以調用其他元件的服務。
通過這樣的元件劃分,元件的開發進度不會受其他業務的影響,可以多個元件單獨的并行開發。元件間的通信都交給中間件來進行,需要通信的類隻需要接觸中間件,而中間件不需要耦合其他元件,這就實作了元件間的解耦。中間件負責處理所有元件之間的排程,在所有元件之間起到控制核心的作用。
這套架構清晰的劃分了不同元件,從整體架構上來限制開發人員進行元件化開發,避免某個開發人員偷懶直接引用頭檔案,産生元件間的耦合,破壞整體架構。假設以後某個業務發生大的改變,需要對相關代碼進行重構,可以在單個元件進行重構。元件化架構降低了重構的風險,保證了代碼的健壯性。
元件內建
元件化架構圖
每個元件都是一個單獨的工程,在元件開發完成後上傳到
git
倉庫。主工程通過
Cocoapods
內建各個元件,內建和更新元件時隻需要
pod update
即可。這樣就是把每個元件當做第三方來管理,管理起來非常友善。
Cocoapods
可以控制每個元件的版本,例如在主項目中復原某個元件到特定版本,就可以通過修改
podfile
檔案實作。選擇
Cocoapods
主要因為其本身功能很強大,可以很友善的內建整個項目,也有利于代碼的複用。通過這種內建方式,可以很好的避免在傳統項目中代碼沖突的問題。
內建方式
對于元件化架構的內建方式,我在看完bang的部落格後專門請教了一下bang。根據在微網誌上和bang的聊天以及其他部落格中的學習,在主項目中內建元件主要分為兩種方式——源碼和
framework
,但都是通過
CocoaPods
來內建。
無論是用
CocoaPods
管理源碼,還是直接管理
framework
,效果都是一樣的,都是可以直接進行
pod update
之類的操作的。
這兩種元件內建方案,實踐中也是各有利弊。直接在主工程中內建代碼檔案,可以在主工程中進行調試。內建
framework
的方式,可以加快編譯速度,而且對每個元件的代碼有很好的保密性。如果公司對代碼安全比較看重,可以考慮
framework
的形式,但
framework
不利于主工程中的調試。
例如手機QQ或者支付寶這樣的大型程式,一般都會采取
framework
的形式。而且一般這樣的大公司,都會有自己的元件庫,這個元件庫往往可以代表一個大的功能或業務元件,直接添加項目中就可以使用。關于元件化庫在後面講淘寶元件化架構的時候會提到。
不推薦的內建方式
之前有些項目是直接用
workspace
的方式內建的,或者直接在原有項目中建立子項目,直接做檔案引用。但這兩點都是不建議做的,因為沒有真正意義上實作業務元件的剝離,隻是像之前的項目一樣從檔案目錄結構上進行了劃分。
元件化開發總結
對于項目架構來說,一定要建立于業務之上來設計架構。不同的項目業務不同,元件化方案的設計也會不同,應該設計最适合公司業務的架構。
架構對比
在除蘑菇街
Protocol
方案外,其他兩種方案都或多或少的存在寫死問題,寫死如果量比較大的話挺麻煩的。
在casatwy的
CTMediator
方案中需要寫死
Target
、
Action
字元串,隻不過這個缺陷被封閉在中間件裡面了,将這些字元串都統一定義為常量,外界使用不需要接觸到寫死。蘑菇街的
MGJRouter
的方案也是一樣的,也有寫死
URL
的問題,蘑菇街可能也做了類似的處理。
casatwy和蘑菇街提出的兩套元件化方案,大體結構是類似的,三套方案都分為調用方、中間件、服務方,隻是在具體實作過程中有些不同。例如
Protocol
方案在中間件中加入了
Protocol
檔案,casatwy的方案在中間件中加入了
Category
。
三種方案内部都有容錯處理,是以三種方案的穩定性都是比較好的,而且都可以拿出來單獨運作,在服務方不存在的情況下也不會有問題。
在三套方案中,服務方都對外提供一個供外界調用的接口類,這個類中實作元件對外提供的服務,中間件通過接口類來實作元件間的通信。在此類中統一定義對外提供的服務,外界調用時就知道服務方可以做什麼。
調用流程也不大一樣,蘑菇街的兩套方案都需要注冊操作,無論是
Block
還是
Protocol
都需要注冊後才可以提供服務。而casatwy的方案則不需要,直接通過
runtime
調用。casatwy的方案實作了真正的對服務方解耦,而蘑菇街的兩套方案則沒有,對服務方和調用方都造成了耦合。
我認為三套方案中,
Protocol
方案是調用和維護最麻煩的一套方案。維護時需要同時維護
Protocol
、接口類兩部分。而且調用時需要将服務方的接口類傳回給調用方,并由調用方執行一系列調用邏輯,調用一個服務的邏輯非常複雜,這在開發中是非常影響開發效率的。
總結
下面是元件化開發中的一個小總結,也是開發過程中的一些注意點。
- 在
方案中,是通過調用MGJRouter
方法并傳入OpenURL:
來發起調用。鑒于URL
協定名等固定格式,可以通過判斷協定名的方式,使用配置表控制URL
和H5
的切換,配置表可以從背景更新,隻需要将協定名更改一下即可。native
mgj://detail?id=123456
http://www.mogujie.com/detail?id=123456
假設現線上上的
native
元件出現嚴重
bug
,在背景将配置檔案中原有的本地
URL
換成
H5
的
URL
,并更新用戶端配置檔案。在調用
MGJRouter
時傳入這個
H5
的
URL
即可完成切換,
MGJRouter
判斷如果傳進來的是一個
H5
的
URL
就直接跳轉
webView
。而且
URL
可以傳遞參數給
MGJRouter
,隻需要
MGJRouter
内部做參數截取即可。
- casatwy方案和蘑菇街
方案,都提供了傳遞明确類型參數的方法。在Protocol
方案中,傳遞參數主要是通過類似MGJRouter
請求一樣在GET
後面拼接參數,和在字典中傳遞參數兩種方式組成。這兩種方式會造成傳遞參數類型不明确,傳遞參數類型受限(URL
請求不能傳遞對象)等問題,後來使用GET
方案彌補這個問題。Protocol
- 元件化開發可以很好的提升代碼複用性,元件可以直接拿到其他項目中使用,這個優點在下面淘寶架構中會着重講一下。
- 對于調試工作,應該放在每個元件中完成。單獨的業務元件可以直接送出給測試提測,這樣測試起來也比較友善。最後元件開發完成并測試通過後,再将所有元件更新到主項目,送出給測試進行內建測試即可。
- 使用元件化架構開發,元件間的通信都是有成本的。是以盡量将業務封裝在元件内部,對外隻提供簡單的接口。即“高内聚、低耦合”原則。
- 把握好劃分粒度的細化程度,太細則項目過于分散,太大則項目元件臃腫。但是項目都是從小到大的一個發展過程,是以不斷進行重構是掌握這個元件的細化程度最好的方式。
我公司架構
下面就簡單說說我公司項目架構,公司項目是一個地圖導航應用,業務層之下的基礎元件占比較大。且基礎元件相對比較獨立,對外提供了很多調用接口。剛開始想的是采用
MGJRouter
的方案,但如果這些調用都通過
Router
進行,開發起來比較複雜,反而會适得其反。最主要我們項目也并不是非常大,沒必要都用
Router
轉發。
對于這個問題,公司項目的架構設計是:層級架構+元件化架構,元件化架構處于層級架構的最上層,也就是業務層。采取這種結構混合的方式進行整體架構,這個對于公共元件的管理和層級劃分比較有利,符合公司業務需求。
公司元件化架構
對于業務層級依然采用元件化架構的設計,這樣可以充分利用元件化架構的優勢,對項目元件間進行解耦。在上層和下層的調用中,下層的功能元件應該對外開放一個接口類,在接口類中聲明所有的服務,實作上層調用目前元件的一個中轉,上層直接調用接口類。這樣做的好處在于,如果下層發生改變不會對上層造成影響,而且也省去了部分
Router
轉發的工作。
在設計層級架構時,需要注意隻能上層對下層依賴,下層對上層不能有依賴,下層中不要包含上層業務邏輯。對于項目中存在的公共資源和代碼,應該将其下沉到下層中。
為什麼這麼做?
首先就像我剛才說的,我公司項目并不是很大,根本沒必要拆分的那麼徹底。
因為元件化開發有一個很重要的原因就是解耦合,如果我做到了底層不對上層依賴,這樣就已經解除了上下層的互相耦合。而且上層對下層進行調用的時候,也不是直接調用下層,通過一個接口類進行中轉,實作了下層的改變對上層無影響,這也是上層對下層解耦的表現。
是以對于第三方就不用說了,上層直接調用下層的第三方也是沒問題的,這都是解耦的。
模型類怎麼辦,放在哪合适?
casatwy對模型類的觀點是去Model化,簡單來說就是用字典代替
Model
存儲資料。這對于元件化架構來說,是解決元件之間資料傳遞的一個很好的方法。
因為模型類是關乎業務的,理論上必須放在業務層也就是業務元件這一層。但是要把模型對象從一個元件中當做參數傳遞到另一個元件中,模型類放在調用方和服務方的哪個元件都不太合适,而且有可能不隻兩個元件使用到這個模型對象。這樣的話在其他元件使用模型對象,必然會造成引用和耦合。
那麼如果把模型類放在
Router
中,這樣會造成
Router
耦合了業務,造成業務的侵入性。如果在用到這個模型對象的所有元件中,都分别維護一份相同的模型類,這樣之後業務發生改變模型類就會很麻煩。
那應該怎麼辦呢?
如果将模型類單獨拉出來,定義一個模型元件呢?這個看起來比較可行,将這個定義模型的元件下沉到下層,模型元件不包含業務,隻聲明模型對象的類。但是一般元件的模型對象都是目前元件内使用的,将模型對象傳遞給其他元件的需求非常少,那所有的模型類都定義到模型元件嗎?
對于這個問題,我建議在項目開發中将模型類還定義在目前業務元件中,在元件間傳遞模型對象時進行去Model化,傳遞字典類型的參數。
上面隻是思考,恰巧我公司持久化方案用的是
CoreData
,所有模型的定義都在
CoreData
元件中,這樣就避免了業務層元件之間因為模型類的耦合。
滴滴元件化架構
之前看過滴滴
iOS
負責人李賢輝的技術分享,分享的是滴滴
iOS
用戶端的架構發展曆程,下面簡單總結一下。
發展曆程
滴滴在最開始的時候架構較混亂。然後在2.0時期重構為
MVC
架構,使項目劃分更加清晰。在3.0時期上線了新的業務線,這時采用的遊戲開發中的狀态機機制,暫時可以滿足現有業務。
然而在後期不斷上線順風車、代駕、巴士等多條業務線的情況下,現有架構變得非常臃腫,代碼耦合嚴重。進而在2015年開始了代号為
“The One”
的方案,這套方案就是滴滴的元件化方案。
架構設計
滴滴的元件化方案,和蘑菇街方案類似,也是通過私有
CocoaPods
來管理各個元件。将整個項目拆分為業務部分和技術部分,業務部分包括專車、拼車、巴士等業務子產品,每個業務子產品就是一個單獨的元件,使用一個
pods
管理。技術部分則分為登入分享、網絡、緩存這樣的一些基礎元件,分别使用不同的
pods
管理。
元件間通信通過
ONERouter
中間件進行通信,
ONERouter
類似于
MGJRouter
,擔負起協調和調用各個元件的作用。元件間通信通過
OpenURL
方法,來進行對應的調用。
ONERouter
内部儲存一份
Class-URL
的映射表,通過
URL
找到
Class
并發起調用,
Class
的注冊放在
+load
方法中進行。
滴滴在元件内部的業務子產品中,子產品内部使用
MVVM+MVCS
混合架構,兩種架構都是
MVC
的衍生版本。其中
MVCS
中的
Store
負責資料相關邏輯,例如訂單狀态、位址管理等資料處理。通過
MVVM
中的
VM
給控制器瘦身,最後
Controller
的代碼量就很少了。
滴滴首頁分析
滴滴文章中說道首頁隻能有一個地圖執行個體,這在很多地圖導航相關應用中都是這樣做的。滴滴首頁主要制器持有導航欄和地圖,每個業務線首頁控制器都添加在主要制器上,并且業務線控制器背景都設定為透明,将透明部分響應事件傳遞到下面的地圖中,隻響應屬于自己的響應事件。
由主要制器來切換各個業務線首頁,切換頁面後根據不同的業務線來更新地圖資料。
淘寶元件化架構
本章節源自于宗心在阿裡技術沙龍上的一次分享
架構發展
淘寶
iOS
用戶端初期是單工程的普通項目,但随着業務的飛速發展,現有架構并不能承載越來越多的業務需求,導緻代碼間耦合很嚴重。後期開發團隊對其不斷進行重構,淘寶
iOS
和
Android
兩個平台,除了某個平台特有的一些特性或某些方案不便實施之外,大體架構都是差不多的。
發展曆程:
- 剛開始是普通的單工程項目,以傳統的
架構進行開發。随着業務不斷的增加,導緻項目非常臃腫、耦合嚴重。MVC
-
2013年淘寶開啟“all in 無線”計劃,計劃将淘寶變為一個大的平台,将阿裡系大多數業務都內建到這個平台上,造成了業務的大爆發。
淘寶開始實行插件化架構,将每個業務子產品劃分為一個元件,将元件以
二方庫的形式內建到主工程。但這種方式并沒有做到真正的拆分,還是在一個工程中使用framework
進行git
,這樣還會造成合并沖突、不好回退等問題。merge
- 迎來淘寶移動端有史以來最大的重構,将其重構為元件化架構。将每個子產品當做一個元件,每個元件都是一個單獨的項目,并且将元件打包成
。主工程通過framework
內建所有元件podfile
,實作業務之間真正的隔離,通過framework
實作元件化架構。CocoaPods
架構優勢
淘寶是使用
git
來做源碼管理的,在插件化架構時需要盡可能避免
merge
操作,否則在大團隊中協作成本是很大的。而使用
CocoaPods
進行元件化開發,則避免了這個問題。
在
CocoaPods
中可以通過
podfile
很好的配置各個元件,包括元件的增加和删除,以及控制某個元件的版本。使用
CocoaPods
的原因,很大程度是為了解決大型項目中,代碼管理工具
merge
代碼導緻的沖突。并且可以通過配置
podfile
檔案,輕松配置項目。
每個元件工程有兩個
target
,一個負責編譯目前元件和運作調試,另一個負責打包
framework
。先在元件工程做測試,測試完成後再內建到主工程中內建測試。
每個元件都是一個獨立
app
,可以獨立開發、測試,使得業務元件更加獨立,所有元件可以并行開發。下層為上層提供能滿足需求的底層庫,保證上層業務層可以正常開發,并将底層庫封裝成
framework
內建到項目中。
使用
CocoaPods
進行元件內建的好處在于,在內建測試自己元件時,可以直接将本地主工程
podfile
檔案中的目前元件指向本地,就可以直接進行內建測試,不需要送出到伺服器倉庫。
淘寶四層架構
淘寶四層架構(圖檔來自淘寶技術分享)
淘寶架構的核心思想是一切皆元件,将工程中所有代碼都抽象為元件。
淘寶架構主要分為四層,最上層是元件
Bundle
(業務元件),依次往下是容器(核心層),中間件
Bundle
(功能封裝),基礎庫
Bundle
(底層庫)。容器層為整個架構的核心,負責元件間的排程和消息派發。
總線設計
總線設計:
URL
路由+服務+消息。統一所有元件的通信标準,各個業務間通過總線進行通信。
總線設計(圖檔來自淘寶技術分享)
URL
可以請求也可以接受傳回值,和
MGJRouter
差不多。
URL
路由請求可以被解析就直接拿來使用,如果不能被解析就跳轉
H5
頁面。這樣就完成了一個對不存在元件調用的相容,使使用者手中比較老的版本依然可以顯示新的元件。
服務提供一些公共服務,由服務方元件負責實作,通過
Protocol
實作。消息負責統一發送消息,類似于通知也需要注冊。
Bundle App
Bundle App(圖檔來自淘寶技術分享)
淘寶提出
Bundle App
的概念,可以通過已有元件,進行簡單配置後就可以組成一個新的
app
出來。解決了多個應用業務複用的問題,防止重複開發同一業務或功能。
Bundle
即
App
,容器即
OS
,所有
Bundle App
被內建到
OS
上,使每個元件的開發就像
app
開發一樣簡單。這樣就做到了從巨型
app
回歸普通
app
的輕盈,使大型項目的開發問題徹底得到了解決。
總結
留個小思考
到目前為止元件化架構文章就寫完了,文章确實挺長的,看到這裡真是辛苦你了?。下面留個小思考,把下面字元串複制到微信輸入框随便發給一個好友,然後點選下面連結大概也能猜到微信的元件化方案。
1 | weixin://dl/profile |
總結
各位可以來我部落格評論區讨論,可以讨論文中提到的技術細節,也可以讨論自己公司架構所遇到的問題,或自己獨到的見解等等。無論是不是架構師或新入行的iOS開發,歡迎各位以一個讨論技術的心态來讨論。在評論區你的問題可以被其他人看到,這樣可能會給其他人帶來一些啟發。
本人部落格位址
現在
H5
技術比較火,好多應用都用
H5
來完成一些頁面的開發,
H5
的跨平台和實時更新等是非常大的優點,但其性能和互動也是缺點。如果以後用戶端能夠發展到可以動态部署線上代碼,不用打包上線應用市場,直接就可以做到原生應用更新,這樣就可以解決原生應用最大的痛點。這段時間公司項目比較忙,有時間我打算研究一下這個技術點?。
Demo
位址:蘑菇街和
casatwy
元件化方案,其
Github
上都給出了
Demo
,這裡就貼出其
Github
位址了。
蘑菇街-MGJRouter
casatwy-CTMediator