該文章屬于劉小壯原創(chuàng)沟突,轉載請注明:劉小壯
前段時間公司項目打算重構营罢,準確來說應該是按之前的產品邏輯重寫一個項目??力图。在重構項目之前涉及到架構選型的問題爽篷,我和組里小伙伴一起研究了一下組件化架構周蹭,打算將項目重構為組件化架構爵嗅。當然不是直接拿來照搬娇澎,還是要根據(jù)公司具體的業(yè)務需求設計架構。
在學習組件化架構的過程中睹晒,從很多高質量的博客中學到不少東西趟庄,例如蘑菇街李忠括细、casatwy
、bang
的博客戚啥。在學習過程中也遇到一些問題奋单,在微博和QQ上和一些做iOS
的朋友進行了交流,非常感謝這些朋友的幫助猫十。
本篇文章主要針對于之前蘑菇街提出的組件化方案览濒,以及casatwy
提出的組件化方案進行分析,后面還會簡單提到滴滴拖云、淘寶贷笛、微信的組件化架構,最后會簡單說一下我公司設計的組件化架構宙项。
組件化架構的由來
隨著移動互聯(lián)網(wǎng)的不斷發(fā)展乏苦,很多程序代碼量和業(yè)務越來越多,現(xiàn)有架構已經(jīng)不適合公司業(yè)務的發(fā)展速度了尤筐,很多都面臨著重構的問題汇荐。
在公司項目開發(fā)中,如果項目比較小盆繁,普通的單工程+MVC架構
就可以滿足大多數(shù)需求了拢驾。但是像淘寶、蘑菇街改基、微信這樣的大型項目繁疤,原有的單工程架構就不足以滿足架構需求了。
就拿淘寶來說秕狰,淘寶在13年開啟的“All in 無線”
戰(zhàn)略中稠腊,就將阿里系大多數(shù)業(yè)務都加入到手機淘寶中,使客戶端出現(xiàn)了業(yè)務的爆發(fā)鸣哀。在這種情況下架忌,單工程架構則已經(jīng)遠遠不能滿足現(xiàn)有業(yè)務需求了。所以在這種情況下我衬,淘寶在13年開啟了插件化架構的重構叹放,后來在14年迎來了手機淘寶有史以來最大規(guī)模的重構,將項目重構為組件化架構挠羔。
蘑菇街的組件化架構
原因
在一個項目越來越大井仰,開發(fā)人員越來越多的情況下,項目會遇到很多問題破加。
- 業(yè)務模塊間劃分不清晰俱恶,模塊之間耦合度很大,非常難維護。
- 所有模塊代碼都編寫在一個項目中合是,測試某個模塊或功能了罪,需要編譯運行整個項目。
為了解決上面的問題聪全,可以考慮加一個中間層來協(xié)調各個模塊間的調用泊藕,所有的模塊間的調用都會經(jīng)過中間層中轉。
但是發(fā)現(xiàn)增加這個中間層后难礼,耦合還是存在的娃圆。中間層對被調用模塊存在耦合,其他模塊也需要耦合中間層才能發(fā)起調用鹤竭。這樣還是存在之前的相互耦合的問題,而且本質上比之前更麻煩了景醇。
架構改進
所以應該做的是臀稚,只讓其他模塊對中間層產生耦合關系,中間層不對其他模塊發(fā)生耦合三痰。
對于這個問題吧寺,可以采用組件化的架構,將每個模塊作為一個組件散劫。并且建立一個主項目稚机,這個主項目負責集成所有組件。這樣帶來的好處是很多的:
- 業(yè)務劃分更佳清晰获搏,新人接手更佳容易赖条,可以按組件分配開發(fā)任務。
- 項目可維護性更強常熙,提高開發(fā)效率纬乍。
- 更好排查問題菩帝,某個組件出現(xiàn)問題棕叫,直接對組件進行處理。
- 開發(fā)測試過程中品抽,可以只編譯自己那部分代碼墓贿,不需要編譯整個項目代碼茧泪。
- 方便集成,項目需要哪個模塊直接通過
CocoaPods
集成即可聋袋。
進行組件化開發(fā)后队伟,可以把每個組件當做一個獨立的app
,每個組件甚至可以采取不同的架構幽勒,例如分別使用MVVM
缰泡、MVC
、MVCS
等架構,根據(jù)自己的編程習慣做選擇棘钞。
MGJRouter方案
蘑菇街通過MGJRouter
實現(xiàn)中間層缠借,由MGJRouter
進行組件間的消息轉發(fā),從名字上來說更像是“路由器”宜猜。實現(xiàn)方式大致是泼返,在提供服務的組件中提前注冊block
,然后在調用方組件中通過URL
調用block
姨拥,下面是調用方式绅喉。
架構設計
MGJRouter
是一個單例對象,在其內部維護著一個“URL -> block”
格式的注冊表叫乌,通過這個注冊表來保存服務方注冊的block
柴罐,以及使調用方可以通過URL
映射出block
,并通過MGJRouter
對服務方發(fā)起調用憨奸。
MGJRouter
是所有組件的調度中心革屠,負責所有組件的調用、切換排宰、特殊處理等操作似芝,可以用來處理一切組件間發(fā)生的關系。除了原生頁面的解析外板甘,還可以根據(jù)URL
跳轉H5
頁面党瓮。
在服務方組件中都對外提供一個PublicHeader
,在PublicHeader
中聲明當前組件所提供的所有功能盐类,這樣其他組件想知道當前組件有什么功能寞奸,直接看PublicHeader
即可。每一個block
都對應著一個URL
在跳,調用方可以通過URL
對block
發(fā)起調用蝇闭。
#ifndef UserCenterPublicHeader_h
#define UserCenterPublicHeader_h
// 跳轉用戶登錄界面
static const NSString * CTBUCUserLogin = @"CTB://UserCenter/UserLogin";
// 跳轉用戶注冊界面
static const NSString * CTBUCUserRegister = @"CTB://UserCenter/UserRegister";
// 獲取用戶狀態(tài)
static const NSString * CTBUCUserStatus = @"CTB://UserCenter/UserStatus";
#endif
在組件內部實現(xiàn)block
的注冊工作,以及block
對外提供服務的代碼實現(xiàn)硬毕。在注冊的時候需要注意注冊時機呻引,應該保證調用時URL
對應的block
已經(jīng)注冊。
蘑菇街項目使用git
作為版本控制工具吐咳,將每個組件都當做一個獨立工程逻悠,并建立主項目來集成所有組件。集成方式是在主項目中通過CocoaPods
來集成韭脊,將所有組件當做二方庫
集成到項目中童谒。詳細的集成技術點在下面“標準組件化架構設計”章節(jié)中會講到。
MGJRouter調用
下面代碼模擬對詳情頁的注冊沪羔、調用饥伊,在調用過程中傳遞id
參數(shù)象浑。參數(shù)傳遞可以有兩種方式,類似于GET
請求在URL
后面拼接參數(shù)琅豆,以及通過字典傳遞參數(shù)愉豺。下面是注冊的示例代碼:
[MGJRouter registerURLPattern:@"mgj://detail" toHandler:^(NSDictionary *routerParameters) {
// 下面可以在拿到參數(shù)后,為其他組件提供對應的服務
NSString uid = routerParameters[@"id"];
}];
通過openURL:
方法傳入的URL
參數(shù)茫因,對詳情頁已經(jīng)注冊的block
方法發(fā)起調用蚪拦。調用方式類似于GET
請求,URL
地址后面拼接參數(shù)冻押。
[MGJRouter openURL:@"mgj://detail?id=404"];
也可以通過字典方式傳參驰贷,MGJRouter
提供了帶有字典參數(shù)的方法,這樣就可以傳遞非字符串之外的其他類型參數(shù)洛巢,例如對象類型參數(shù)括袒。
[MGJRouter openURL:@"mgj://detail" withParam:@{@"id" : @"404"}];
組件間傳值
有的時候組件間調用過程中,需要服務方在完成調用后返回相應的參數(shù)稿茉。蘑菇街提供了另外的方法锹锰,專門來完成這個操作。
[MGJRouter registerURLPattern:@"mgj://cart/ordercount" toObjectHandler:^id(NSDictionary *routerParamters){
return @42;
}];
通過下面的方式發(fā)起調用狈邑,并獲取服務方返回的返回值城须,要做的就是傳遞正確的URL
和參數(shù)即可蚤认。
NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"];
短鏈管理
這時候會發(fā)現(xiàn)一個問題米苹,在蘑菇街組件化架構中,存在了很多硬編碼的URL
和參數(shù)砰琢。在代碼實現(xiàn)過程中URL
編寫出錯會導致調用失敗蘸嘶,而且參數(shù)是一個字典類型,調用方不知道服務方需要哪些參數(shù)陪汽,這些都是個問題训唱。
對于這些數(shù)據(jù)的管理,蘑菇街開發(fā)了一個web
頁面挚冤,這個web
頁面統(tǒng)一來管理所有的URL
和參數(shù)况增,Android
和iOS
都使用這一套URL
,可以保持統(tǒng)一性训挡。
基礎組件
在項目中存在很多公共部分的東西澳骤,例如封裝的網(wǎng)絡請求、緩存澜薄、數(shù)據(jù)處理等功能为肮,以及項目中所用到的資源文件。蘑菇街將這些部分也當做組件肤京,劃分為基礎組件颊艳,位于業(yè)務組件下層。所有業(yè)務組件都使用同一套基礎組件,也可以保證公共部分的統(tǒng)一性棋枕。
Protocol方案
整體架構
為了解決MGJRouter
方案中URL
硬編碼白修,以及字典參數(shù)類型不明確等問題,蘑菇街在原有組件化方案的基礎上推出了Protocol
方案戒悠。Protocol
方案由兩部分組成熬荆,進行組件間通信的ModuleManager
類以及MGJComponentProtocol
協(xié)議類。
通過中間件ModuleManager
進行消息的調用轉發(fā)绸狐,在ModuleManager
內部維護一張映射表卤恳,映射表由之前的"URL -> block"
變成"Protocol -> Class"
。
在中間件中創(chuàng)建MGJComponentProtocol
文件寒矿,服務方組件將可以用來調用的方法都定義在Protocol
中突琳,將所有服務方的Protocol
都分別定義到MGJComponentProtocol
文件中,如果協(xié)議比較多也可以分開幾個文件定義符相。這樣所有調用方依然是只依賴中間件拆融,不需要依賴除中間件之外的其他組件。
Protocol
方案中每個組件需要一個MGJModuleImplement
啊终,此類負責實現(xiàn)當前組件對應的協(xié)議方法镜豹,也就是對外提供服務的實現(xiàn)。在程序開始運行時將自身的Class
注冊到ModuleManager
中蓝牲,并將Protocol
反射為字符串當做key
趟脂。
Protocol
方案依然需要提前注冊服務,由于Protocol
方案是返回一個Class
例衍,并將Class
反射為對象再調用方法昔期,這種方式不會直接調用類的內部邏輯》鹦可以將Protocol
方案的Class
注冊硼一,都放在類對應的MGJModuleImplement
中,或者專門建立一個RegisterProtocol
類梦抢。
示例代碼
創(chuàng)建MGJUserImpl
類當做User
組件對外公開的類般贼,并在MGJComponentProtocol.h
中定義MGJUserProtocol
協(xié)議,由MGJUserImpl
類實現(xiàn)協(xié)議中定義的方法奥吩,完成對外提供服務的過程哼蛆。下面是協(xié)議定義:
@protocol MGJUserProtocol <NSObject>
- (NSString *)getUserName;
@end
Class
遵守協(xié)議并實現(xiàn)定義的方法,外界通過Protocol
獲取的Class
并實例化為對象圈驼,調用服務方實現(xiàn)的協(xié)議方法人芽。
ModuleManager
的協(xié)議注冊方法,注冊時將Protocol
反射為字符串當做存儲的key
绩脆,將實現(xiàn)協(xié)議的Class
當做值存儲萤厅。通過Protocol
取Class
的時候橄抹,就是通過Protocol
從ModuleManager
中將Class
映射出來。
[ModuleManager registerClass:MGJUserImpl forProtocol:@protocol(MGJUserProtocol)];
調用時通過Protocol
從ModuleManager
中映射出注冊的Class
惕味,將獲取到的Class
實例化楼誓,并調用Class
實現(xiàn)的協(xié)議方法完成服務調用。
Class cls = [[ModuleManager sharedInstance] classForProtocol:@protocol(MGJUserProtocol)];
id userComponent = [[cls alloc] init];
NSString *userName = [userComponent getUserName];
項目調用流程
蘑菇街是MGJRouter
和Protocol
混用的方式名挥,兩種實現(xiàn)的調用方式不同疟羹,但大體調用邏輯和實現(xiàn)思路類似。在MGJRouter
不能滿足需求或調用不方便時禀倔,就可以通過Protocol
的方式調用榄融。
在進入程序后,先使用
MGJRouter
對服務方組件進行注冊救湖。每個URL
對應一個block
的實現(xiàn)愧杯,block
中的代碼就是組件對外提供的服務,調用方可以通過URL
調用這個服務鞋既。調用方通過
MGJRouter
調用openURL:
方法力九,并將被調用代碼對應的URL
傳入,MGJRouter
會根據(jù)URL
查找對應的block
實現(xiàn)邑闺,從而調用組件的代碼進行通信跌前。調用和注冊
block
時,block
有一個字典用來傳遞參數(shù)陡舅。這樣的優(yōu)勢就是參數(shù)類型和數(shù)量理論上是不受限制的抵乓,但是需要很多硬編碼的key
名在項目中。
內存管理
蘑菇街組件化方案有兩種蹭沛,Protocol
和MGJRouter
的方式臂寝,但都需要進行register
操作章鲤。Protocol
注冊的是Class
摊灭,MGJRouter
注冊的是Block
,注冊表是一個NSMutableDictionary
類型的字典败徊,而字典的擁有者又是一個單例對象帚呼,這樣會造成內存的常駐。
下面是對兩種實現(xiàn)方式內存消耗的分析:
首先說一下
MGJRouter
方案可能導致的內存問題皱蹦,由于block
會對代碼塊內部對象進行持有煤杀,如果使用不當很容易造成內存泄漏的問題。
block
自身實際上不會造成很大的內存泄漏沪哺,主要是內部引用的變量沈自,所以在使用時就需要注意強引用的問題,并適當使用weak
修飾對應的變量辜妓。以及在適當?shù)臅r候枯途,釋放對應的變量忌怎。
除了對外部變量的引用,在block
代碼塊內部盡量不要直接創(chuàng)建對象酪夷,應該通過方法調用中轉一下榴啸。對于協(xié)議這種實現(xiàn)方式,和
block
內存常駐方式差不多晚岭。只是將存儲的block
對象換成Class
對象鸥印。這實際上是存儲的類對象,類對象本來就是單例模式坦报,所以不會造成多余內存占用库说。
casatwy組件化方案
整體架構
casatwy
組件化方案可以處理兩種方式的調用,遠程調用和本地調用片择,對于兩個不同的調用方式分別對應兩個接口璃弄。
遠程調用通過
AppDelegate
代理方法傳遞到當前應用后,調用遠程接口并在內部做一些處理构回,處理完成后會在遠程接口內部調用本地接口夏块,以實現(xiàn)本地調用為遠程調用服務。本地調用由
performTarget:action:params:
方法負責纤掸,但調用方一般不直接調用performTarget:
方法脐供。CTMediator
會對外提供明確參數(shù)和方法名的方法,在方法內部調用performTarget:
方法和參數(shù)的轉換借跪。
架構設計思路
casatwy
是通過CTMediator
類實現(xiàn)組件化的政己,在此類中對外提供明確參數(shù)類型的接口,接口內部通過performTarget
方法調用服務方組件的Target
掏愁、Action
歇由。由于CTMediator
類的調用是通過runtime
主動發(fā)現(xiàn)服務的,所以服務方對此類是完全解耦的果港。
但如果CTMediator
類對外提供的方法都放在此類中沦泌,將會對CTMediator
造成極大的負擔和代碼量。解決方法就是對每個服務方組件創(chuàng)建一個CTMediator
的Category
辛掠,并將對服務方的performTarget
調用放在對應的Category
中谢谦,這些Category
都屬于CTMediator
中間件,從而實現(xiàn)了感官上的接口分離萝衩。
對于服務方的組件來說回挽,每個組件都提供一個或多個Target
類,在Target
類中聲明Action
方法猩谊。Target
類是當前組件對外提供的一個“服務類”千劈,Target
將當前組件中所有的服務都定義在里面,CTMediator
通過runtime
主動發(fā)現(xiàn)服務牌捷。
在Target
中的所有Action
方法墙牌,都只有一個字典參數(shù)袁梗,所以可以傳遞的參數(shù)很靈活,這也是casatwy
提出的去Model
化的概念憔古。在Action
的方法實現(xiàn)中遮怜,對傳進來的字典參數(shù)進行解析,再調用組件內部的類和方法鸿市。
架構分析
casatwy
為我們提供了一個Demo锯梁,通過這個Demo
可以很好的理解casatwy
的設計思路,下面按照我的理解講解一下這個Demo
焰情。
打開Demo
后可以看到文件目錄非常清楚陌凳,在上圖中用藍框框出來的就是中間件部分,紅框框出來的就是業(yè)務組件部分内舟。我對每個文件夾做了一個簡單的注釋合敦,包含了其在架構中的職責。
在CTMediator
中定義遠程調用和本地調用的兩個方法验游,其他業(yè)務相關的調用由Category
完成充岛。
// 遠程App調用入口
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;
// 本地組件調用入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;
在CTMediator
中定義的ModuleA
的Category
,為其他組件提供了一個獲取控制器并跳轉的功能耕蝉,下面是代碼實現(xiàn)崔梗。由于casatwy
的方案中使用performTarget
的方式進行調用,所以涉及到很多硬編碼字符串的問題垒在,casatwy
采取定義常量字符串來解決這個問題蒜魄,這樣管理也更方便。
#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
的方式調用伞鲫,這個過程就叫做發(fā)現(xiàn)服務。
在Target_A
中對傳遞的參數(shù)做了處理耘成,以及內部的業(yè)務邏輯實現(xiàn)榔昔。方法是發(fā)生在ModuleA
內部的驹闰,這樣就可以保證組件內部的業(yè)務不受外部影響瘪菌,對內部業(yè)務沒有侵入性。
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params {
// 對傳過來的字典參數(shù)進行解析嘹朗,并調用ModuleA內部的代碼
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}
命名規(guī)范
在大型項目中代碼量比較大师妙,需要避免命名沖突的問題。對于這個問題casatwy
采取的是加前綴的方式屹培,從casatwy
的Demo
中也可以看出默穴,其組件ModuleA
的Target
命名為Target_A
怔檩,可以區(qū)分各個組件的Target
。被調用的Action
命名為Action_nativeFetchDetailViewController:
蓄诽,可以區(qū)分組件內的方法與對外提供的方法薛训。
casatwy
將類和方法的命名,都統(tǒng)一按照其功能做區(qū)分當做前綴仑氛,這樣很好的將組件相關和組件內部代碼進行了劃分乙埃。
結果分析
Protocol
從我調研和使用的結果來說,并不推薦使用Protocol
方案锯岖。首先Protocol
方案的代碼量就比MGJRouter
方案的要多介袜,調用和注冊代碼量很大,調用起來并不是很方便出吹。
本質上來說Protocol
方案是通過類對象實例一個變量遇伞,并調用變量的方法,并沒有真正意義上的改變組件之間的交互方案捶牢,但MGJRouter
的方案卻通過URL Router
的方式改變和統(tǒng)一了組件間調用方式鸠珠。
并且Protocol
沒有對Remote Router
的支持,不能直接處理來自Push
的調用秋麸,在靈活性上就不如MGJRouter
的方案跳芳。
CTMediator
我并不推薦CTMediator
方案,這套方案實際上是一套很臃腫的方案竹勉。雖然為CTMediator
提供了很多Category
飞盆,但實際上組件間的調用邏輯都耦合在了中間件中涣脚。同樣嵌削,和Protocol
方案存在一個相同的問題,就是調用代碼量很大尊浪,使用起來并不方便票腰。
在CTMediator
方案中存在很多硬編碼的問題城看,例如target
、action
以及參數(shù)名都是硬編碼在中間件中的杏慰,這種調用方式并不靈活直接测柠。
但casatwy
提出了去Model
化的想法,我覺得這在組件化中傳參來說缘滥,是非常靈活的轰胁,這點我比較認同。相對于MGJRouter
的話朝扼,也采用了去Model
化的傳參方式赃阀,而不是直接傳遞模型對象。組件化傳參并不適用傳模型對象擎颖,但組件內部還是可以使用Model
的榛斯。
MGJRouter
MGJRouter
方案是一套非常輕量級的方案观游,其中間件代碼總共也就兩百行以內,非常簡潔驮俗。在調用時直接通過URL
調用懂缕,調用起來很簡單,我推薦使用這套方案作為組件化架構的中間件王凑。
MGJRouter
最強大的一點在于提佣,統(tǒng)一了遠程調用和本地調用。這就使得可以通過Push
的方式荤崇,進行任何允許的組件間調用拌屏,對項目運營是有很大幫助的。
這三套方案都實現(xiàn)了組件間的解耦术荤,MGJRouter
和Protocol
都是調用方對中間件的耦合倚喂,CTMediator
是中間件對組件的耦合,都是單向耦合瓣戚。
接口類
在三套方案中端圈,服務方組件都對外提供一個PublicHeader
或Target
,在文件中統(tǒng)一定義對外提供的服務子库,組件間通信的實現(xiàn)代碼大多數(shù)都在里面舱权。
但三套實現(xiàn)方案實現(xiàn)方式并不同,蘑菇街的兩套方案都需要注冊操作仑嗅,無論是Block
還是Protocol
都需要注冊后才可以提供服務宴倍。而casatwy
的方案則不需要,直接通過runtime
調用仓技。
組件化架構設計
在上面文章中提到了casatwy
方案的CTMediator
鸵贬,蘑菇街方案的MGJRouter
和ModuleManager
,之后將統(tǒng)稱為中間件脖捻,下面讓我們設計一套組件化架構阔逼。
整體架構
組件化架構中,需要一個主工程地沮,主工程負責集成所有組件嗜浮。每個組件都是一個單獨的工程,創(chuàng)建不同的git
私有倉庫來管理摩疑,每個組件都有對應的開發(fā)人員負責開發(fā)危融。開發(fā)人員只需要關注與其相關組件的代碼,不用考慮其他組件未荒,這樣來新人也好上手专挪。
組件的劃分需要注意組件粒度,粒度根據(jù)業(yè)務可大可小片排。組件劃分可以將每個業(yè)務模塊都劃分為組件寨腔,對于網(wǎng)絡、數(shù)據(jù)庫等基礎模塊率寡,也應該劃分到組件中迫卢。項目中會用到很多資源文件、配置文件等冶共,也應該劃分到對應的組件中乾蛤,避免重復的資源文件。項目實現(xiàn)完全的組件化捅僵。
每個組件都需要對外提供調用家卖,在對外公開的類或組件內部,注冊對應的URL
庙楚。組件處理中間件調用的代碼應該對其他代碼無侵入上荡,只負責對傳遞過來的數(shù)據(jù)進行解析和組件內調用的功能。
組件集成
每個組件都是一個單獨的工程馒闷,在組件開發(fā)完成后上傳到git
倉庫酪捡。主工程通過Cocoapods
集成各個組件,集成和更新組件時只需要pod update
即可纳账。這樣就是把每個組件當做第三方來管理逛薇,管理起來非常方便。
Cocoapods
可以控制每個組件的版本疏虫,例如在主項目中回滾某個組件到特定版本永罚,就可以通過修改podfile
文件實現(xiàn)。選擇Cocoapods
主要因為其本身功能很強大卧秘,可以很方便的集成整個項目尤蛮,也有利于代碼的復用。通過這種集成方式斯议,可以很好的避免在傳統(tǒng)項目中代碼沖突的問題产捞。
集成方式
對于組件化架構的集成方式,我在看完bang
的博客后專門請教了一下bang
哼御。根據(jù)在微博上和bang
的聊天以及其他博客中的學習坯临,在主項目中集成組件主要分為兩種方式——源碼和framework
,但都是通過CocoaPods
來集成恋昼。
無論是用CocoaPods
管理源碼看靠,還是直接管理framework
,集成方式都是一樣的液肌,都是直接進行pod update
等CocoaPods
操作挟炬。
這兩種組件集成方案,實踐中也是各有利弊。直接在主工程中集成代碼文件谤祖,可以看到其內部實現(xiàn)源碼婿滓,方便在主工程中進行調試。集成framework
的方式粥喜,可以加快編譯速度凸主,而且對每個組件的代碼有很好的保密性。如果公司對代碼安全比較看重额湘,可以考慮framework
的形式卿吐。
例如手機QQ或者支付寶這樣的大型程序,一般都會采取framework
的形式锋华。而且一般這樣的大公司嗡官,都會有自己的組件庫,這個組件庫往往可以代表一個大的功能或業(yè)務組件毯焕,直接添加項目中就可以使用衍腥。關于組件化庫在后面講淘寶組件化架構的時候會提到。
資源文件
對于項目中圖片的集成芥丧,可以把圖片當做一個單獨的組件紧阔,組件中只存在圖片文件,沒有任何代碼续担。圖片可以使用Bundle
和image assets
進行管理擅耽,如果是Bundle
就針對不同業(yè)務模塊建立不同的Bundle
,如果是image assets
物遇,就按照不同的模塊分類建立不同的assets
乖仇,將所有資源放在同一個組件內。
Bundle
和image assets
兩者相比询兴,我還是更推薦用assets
的方式乃沙,因為assets
自身提供很多功能(例如設置圖片拉伸范圍),而且在打包之后圖片會被打包在.cer
文件中诗舰,不會被看到警儒。(現(xiàn)在也可以通過工具對.cer
文件進行解析,獲取里面的圖片)
使用Cocoapods
眶根,所有的資源文件都放置在一個podspec
中蜀铲,主工程可以直接引用這個podspec
,假設此podspec
名為:Assets
属百,而這個Assets
的podspec
里面配置信息可以寫為:
s.resources = "Assets/Assets.xcassets/ ** / *.{png}"
主工程則直接在podfile
文件中加入:
pod 'Assets', :path => '../MainProject/Assets'(這種寫法是訪問本地的记劝,可以換成git)
這樣即可在主工程直接訪問到Assets
中的資源文件(不局限圖片,sqlite
族扰、js
厌丑、html
亦可定欧,在s.resources
設置好配置信息即可)了。
優(yōu)點
組件化開發(fā)可以很好的提升代碼復用性怒竿,組件可以直接拿到其他項目中使用砍鸠,這個優(yōu)點在下面淘寶架構中會著重講一下。
對于調試工作愧口,可以放在每個組件中完成睦番。單獨的業(yè)務組件可以直接提交給測試使用类茂,這樣測試起來也比較方便耍属。最后組件開發(fā)完成并測試通過后,再將所有組件更新到主項目巩检,提交給測試進行集成測試即可厚骗。
通過這樣的組件劃分,組件的開發(fā)進度不會受其他業(yè)務的影響兢哭,可以多個組件并行開發(fā)领舰。組件間的通信都交給中間件來進行,需要通信的類只需要接觸中間件迟螺,而中間件不需要耦合其他組件冲秽,這就實現(xiàn)了組件間的解耦。中間件負責處理所有組件之間的調度矩父,在所有組件之間起到控制核心的作用锉桑。
組件化框架清晰的劃分了不同模塊,從整體架構上來約束開發(fā)人員進行組件化開發(fā)窍株,實現(xiàn)了組件間的物理隔離民轴。組件化架構在各個模塊之間天然形成了一道屏障,避免某個開發(fā)人員偷懶直接引用頭文件球订,產生組件間的耦合后裸,破壞整體架構。
使用組件化架構進行開發(fā)時冒滩,因為每個人都負責自己的組件微驶,代碼提交也只提交自己負責模塊的倉庫,所以代碼沖突的問題會變得很少开睡。
假設以后某個業(yè)務發(fā)生大的改變因苹,需要對相關代碼進行重構,可以在單個組件內進行重構士八。組件化架構降低了重構的風險容燕,保證了代碼的健壯性。
架構分析
在MGJRouter
方案中婚度,是通過調用OpenURL:
方法并傳入URL
來發(fā)起調用的蘸秘。鑒于URL
協(xié)議名等固定格式官卡,可以通過判斷協(xié)議名的方式,使用配置表控制H5
和native
的切換醋虏,配置表可以從后臺更新寻咒,只需要將協(xié)議名更改一下即可。
mgj://detail?id=123456
http://www.mogujie.com/detail?id=123456
假設現(xiàn)在線上的native
組件出現(xiàn)嚴重bug
颈嚼,在后臺將配置文件中原有的本地URL
換成H5
的URL
毛秘,并更新客戶端配置文件。
在調用MGJRouter
時傳入這個H5
的URL
即可完成切換阻课,MGJRouter
判斷如果傳進來的是一個H5
的URL
就直接跳轉webView
叫挟。而且URL
可以傳遞參數(shù)給MGJRouter
,只需要MGJRouter
內部做參數(shù)截取即可限煞。
使用組件化架構開發(fā)抹恳,組件間的通信都是有成本的。所以盡量將業(yè)務封裝在組件內部署驻,對外只提供簡單的接口奋献。即“高內聚、低耦合”原則旺上。
把握好組件劃分粒度的細化程度瓶蚂,太細則項目過于分散,太大則項目組件臃腫宣吱。但是項目都是從小到大的一個發(fā)展過程窃这,所以不斷進行重構是掌握這個組件的細化程度最好的方式。
注意點
如果通過framework
等二進制形式凌节,將組件集成到主項目中钦听,需要注意預編譯指令的使用。因為預編譯指令在打包framework
的時候倍奢,就已經(jīng)在組件二進制代碼中打包好朴上,到主項目中的時候預編譯指令其實已經(jīng)不再起作用了,而是已經(jīng)在打包時按照預編譯指令編碼為固定二進制卒煞。
我公司架構
對于項目架構來說痪宰,一定要建立于業(yè)務之上來設計架構。不同的項目業(yè)務不同畔裕,組件化方案的設計也會不同衣撬,應該設計最適合公司業(yè)務的架構。
架構設計
我公司項目是一個地圖導航應用扮饶,業(yè)務層之下的核心模塊和基礎模塊占比較大具练,涉及到地圖SDK、算路甜无、語音等模塊扛点。且基礎模塊相對比較獨立哥遮,對外提供了很多調用接口。由此可以看出陵究,公司項目是一個重邏輯的項目眠饮,不像電商等App
偏展示。
項目整體的架構設計是:層級架構+組件化架構铜邮,對于具體的實現(xiàn)細節(jié)會在下面詳細講解仪召。采取這種結構混合的方式進行整體架構,對于組件的管理和層級劃分比較有利松蒜,符合公司業(yè)務需求扔茅。
在設計架構時,我們將整個項目都拆分為組件牍鞠,組件化程度相當高咖摹。用到哪個組件就在工程中通過Podfile
進行集成评姨,并通過URLRouter
統(tǒng)一所有組件間的通信难述。
組件化架構是項目的整體框架,而對于框架中每個業(yè)務模塊的實現(xiàn)吐句,可以是任意方式的架構胁后,MVVM
、MVC
嗦枢、MVCS
等都是可以的攀芯,只要通過MGJRouter
將組件間的通信方式統(tǒng)一即可。
分層架構
組件化架構在物理結構上來說是不分層次的文虏,只有組件與組件之間的劃分關系侣诺。但是在組件化架構的基礎上,應該根據(jù)項目和業(yè)務設計自己的層次架構氧秘,這套層次架構可以用來區(qū)分組件所處的層次及職責年鸳,所以我們設計了層級架構+組件化架構的整體架構。
我公司項目最開始設計的是三層架構:業(yè)務層 -> 核心層 (high + low) -> 基礎層
丸相,其中核心層又分為high
和low
兩部分搔确。但是這種架構會造成核心層過重,基礎層過輕的問題灭忠,這種并不適合組件化架構膳算。
在三層架構中會發(fā)現(xiàn),low
層并沒有耦合業(yè)務邏輯弛作,在同層級中是比較獨立的涕蜂,職責較為單一和基礎。我們對low
層下沉到基礎層中映琳,并和基礎層進行合并机隙。所以架構被重新分為三層架構:業(yè)務層 -> 核心層 -> 基礎層
瘦真。之前基礎層大多是資源文件和配置文件,在項目中存在感并不高黍瞧。
在分層架構中诸尽,需要注意只能上層對下層依賴,下層對上層不能有依賴印颤,下層中不要包含上層業(yè)務邏輯您机。對于項目中存在的公共資源和代碼,應該將其下沉到下層中年局。
職責劃分
在三層架構中际看,業(yè)務層負責處理上層業(yè)務,將不同業(yè)務劃分到相應組件中矢否,例如IM
組件仲闽、導航組件、用戶組件等僵朗。業(yè)務層的組件間關系比較復雜赖欣,會涉及到組件間業(yè)務的通信,以及業(yè)務層組件對下層組件的引用验庙。
核心層位于業(yè)務層下方顶吮,為業(yè)務層提供業(yè)務支持,如網(wǎng)絡粪薛、語音識別等組件應該劃分到核心層悴了。核心層應該盡量減少組件間的依賴,將依賴降到最小违寿。核心層有時相互之間也需要支持湃交,例如經(jīng)緯度組件需要網(wǎng)絡組件提供網(wǎng)絡請求的支持,這種是不可避免的藤巢。
其他比較基礎的模塊搞莺,都放在基礎層當做基礎組件。例如AFN
菌瘪、地圖SDK
腮敌、加密算法等,這些組件都比較獨立且不摻雜任何業(yè)務邏輯俏扩,職責更加單一糜工,相對于核心層更底層÷嫉可以包含第三方庫捌木、資源文件、配置文件嫉戚、基礎庫等幾大類刨裆,基礎層組件相互之間不應該產生任何依賴澈圈。
在設計各個組件時,應該遵循“高內聚帆啃,低耦合”的設計規(guī)范瞬女,組件的調用應該簡單且直接,減少調用方的其他處理努潘。對于核心層和基礎層的劃分诽偷,可以以是否涉及業(yè)務、是否涉及同級組件間通信疯坤、是否經(jīng)常改動為參照點漠烧。如果符合這幾點則放在核心層掌唾,如果不符合則放在基礎層。
集成方式
新建一個項目后蚤告,首先將配置文件已慢、URLRouter
远豺、App
容器等集成到主工程中熊户,做一些基礎的項目配置坑匠,隨后集成需要的組件即可。項目被整體拆分為組件化架構后突梦,應用對所有組件的集成方式都是一樣的诫舅,通過Podfile
將需要的組件集成到項目中。通過組件化的方式宫患,使得開發(fā)新項目速度變得非常快这弧。
在集成業(yè)務層和核心層組件后娃闲,組件間的通信都是由URLRouter
進行通信,項目中不允許直接依賴組件源碼匾浪。而基礎層組件則在集成后直接依賴皇帮,例如資源文件和配置文件,這些都是直接在主工程或組件中使用的蛋辈。第三方庫則是通過核心層的業(yè)務封裝属拾,封裝后由URLRouter
進行通信,但核心層也是直接依賴第三方庫源碼的冷溶。
組件的集成方式有兩種渐白,源碼和framework
的形式,我們使用framework
的方式集成逞频。因為一般都是項目比較大才用組件化的纯衍,但大型項目都會存在編譯時間的問題,如果通過framework
則會大大減少編譯時間苗胀,可以節(jié)省開發(fā)人員的時間襟诸。
組件間通信
對于組件間通信瓦堵,我們采用的MGJRouter
方案。因為MGJRouter
現(xiàn)在已經(jīng)很穩(wěn)定了歌亲,而且可以滿足蘑菇街這樣量級的App
需求菇用,證明是很好的,沒必要自己寫一套再慢慢踩坑陷揪。
MGJRouter
的好處在于刨疼,其調用方式很靈活,通過MGJRouter
注冊并在block
中處理回調鹅龄,通過URL
直接調用或者URL+Params
字典的方式進行調用揩慕。由于通過URL
拼接參數(shù)或Params
字典傳值,所以其參數(shù)類型沒有數(shù)量限定扮休,傳遞比較靈活迎卤。在通過openURL:
調用后,可以在completionBlock
中處理完成邏輯玷坠。
MGJRouter
有個問題在于蜗搔,在編寫組件間通信的代碼時,會涉及到大量的Hardcode
八堡。對于Hardcode
的問題樟凄,蘑菇街開發(fā)了一套后臺系統(tǒng),將所有的Router
需要的URL
和參數(shù)名兄渺,都定義到這套系統(tǒng)中缝龄。我們維護了一個Plist
表,內部按不同組件進行劃分挂谍,包含URL
和傳參名以及回調參數(shù)叔壤。
路由層安全
組件化架構需要注意路由層的安全問題。MGJRouter
方案可以處理本地及遠程的OpenURL
調用口叙,如果是程序內組件間的OpenURL
調用炼绘,則不需要進行校驗。而跨應用的OpenURL
調用妄田,則需要進行合法性檢查俺亮。這是為了防止第三方偽造進行OpenURL
調用,所以對應用外調起的OpenURL
進行的合法性檢查疟呐,例如其他應用調起脚曾、服務器Remote Push
等。
在合法性檢查的設計上萨醒,每個從應用外調起的合法URL
都會帶有一個token
斟珊,在本地會對token
進行校驗。這種方式的優(yōu)勢在于,沒有網(wǎng)絡請求的限制和延時囤踩。
代理方法
在項目中經(jīng)常會用到代理模式傳值旨椒,代理模式在iOS
中主要分為三部分,協(xié)議堵漱、代理方综慎、委托方三部分。
但如果使用組件化架構的話勤庐,會涉及到組件與組件間的代理傳值示惊,代理方需要設置為委托方的delegate
,但組件間是不可以直接產生耦合的愉镰。對于這種跨組件的代理情況米罚,我們直接將代理方的對象通過MGJRouter
以參數(shù)的形式傳給另一個組件,在另一個組件中進行代理設置丈探。
HomeViewController *homeVC = [[HomeViewController alloc] init];
NSDictionary *params = @{CTBUserCenterLoginDelegateKey : homeVC};
[MGJRouter openURL:@"CTB://UserCenter/UserLogin" withUserInfo:params completion:nil];
[MGJRouter registerURLPattern:@"CTB://UserCenter/UserLogin" toHandler:^(NSDictionary *routerParameters) {
UIViewController *homeVC = routerParameters[CTBUserCenterLoginDelegateKey];
LoginViewController *loginVC = [[LoginViewController alloc] init];
loginVC.delegate = homeVC;
}];
協(xié)議的定義放在委托方組件的PublicHeader.h
中录择,代理方組件只引用這個PublicHeader.h
文件,不耦合委托方內部代碼碗降。為了避免定義的代理方法中出現(xiàn)耦合的情況隘竭,方法中不能出現(xiàn)和組件內部業(yè)務有關的對象,只能傳遞系統(tǒng)的類讼渊。如果涉及到交互的情況动看,則通過協(xié)議方法的返回值進行。
組件傳參
MGJRouter
可以在openURL:
時傳入一個NSDictionary
參數(shù)爪幻,在接觸RAC
之后菱皆,我在想是不是可以把NSDictionary
參數(shù)變?yōu)?code>RACSignal參數(shù),直接傳一個信號過去笔咽。
注冊MGJRouter
:
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"劉小壯"];
return [RACDisposable disposableWithBlock:^{
NSLog(@"disposable");
}];
}];
[MGJRouter registerURLPattern:@"CTB://UserCenter/getUserInfo" withSignal:signal];
調用MGJRouter
:
RACSignal *signal = [MGJRouter openURL:@"CTB://UserCenter/getUserInfo"];
[signal subscribeNext:^(NSString *userName) {
NSLog(@"userName %@", userName);
}];
這種方式是可行的搔预。使用RACSignal
方式優(yōu)點在于,相對于直接傳字典過去更加靈活叶组,并且具備RAC
的諸多特性。但缺點也不少历造,信號控制不好亂用的話也很容易挖坑甩十,是否使用還是看團隊情況了。
常量定義
在項目中經(jīng)常會定義一些常量吭产,例如通知名侣监、常量字符串等,這些常量一般都和所屬組件有很強的關系臣淤,不好單獨拆出來放到其他組件橄霉。但是這些變量數(shù)量并不是很多,而且不是每個組件中都有邑蒋。
所以姓蜂,我們將這些變量都聲明在PublicHeader.h
文件中按厘,其他組件只能引用PublicHeader.h
文件,不能引用組件內部業(yè)務代碼钱慢,這樣就規(guī)避掉了組件間耦合的問題逮京。
H5和Native通信
在項目中經(jīng)常會用到H5
頁面,如果能通過點擊H5
頁面調起原生頁面束莫,這樣的話Native
和H5
的融合會更好懒棉。所以我們設計了一套H5
和Native
交互的方案,這套方案可以使用URLRouter
的方式調起原生頁面览绿,實現(xiàn)方式也很簡單策严,并且這套方案和H5
原本的跳轉邏輯并不沖突。
通過iOS
自帶UIWebView
創(chuàng)建一個H5
頁面后饿敲,H5
可以通過調用下面的JS
函數(shù)和Native
通信妻导。調用時可以傳入新的URL
,這個URL
可以設置為URLRouter
的URL
诀蓉。
window.location.href = 'CTB://UserCenter/UserLogin?userName=lxz&WeChatID=lz2046703959';
通過JS
刷新H5
頁面時栗竖,會調用下面的代理方法。如果方法返回YES
渠啤,則會根據(jù)URL
協(xié)議進行跳轉狐肢。
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
跳轉時系統(tǒng)會判斷通信協(xié)議,如果是HTTP
等標準協(xié)議沥曹,則會在當前頁面進行刷新份名。如果跳轉協(xié)議在URL Schame
中注冊,則會通過系統(tǒng)openURL:
的方式調用到AppDelegate
的系統(tǒng)代理方法中妓美,在代理方法中調用URLRouter
僵腺,則可以通過H5
頁面喚起原生頁面。
AppService
在應用啟動過程中壶栋,通常會做一些初始化操作辰如。有些初始化操作是運行程序所需要的,例如崩潰統(tǒng)計贵试、建立服務器的長連接等琉兜。或有的組件會對初始化操作有依賴關系毙玻,例如網(wǎng)絡組件依賴requestToken
等豌蟋。
對于應用啟動時的初始化操作,應該創(chuàng)建一個AppService
來統(tǒng)一管理啟動操作桑滩,將初始化操作都放在里面梧疲,包含創(chuàng)建根控制器等。其中有的初始化操作需要盡快執(zhí)行,有的并不需要立即執(zhí)行幌氮,可以根據(jù)不同操作設定優(yōu)先級缭受,來管理所有初始化操作。
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSUInteger, CTBAppServicePriority) {
CTBAppServicePriorityLow,
CTBAppServicePriorityDefault,
CTBAppServicePriorityHigh,
};
@interface CTBAppService : NSObject
+ (instancetype)appService;
- (void)registerService:(dispatch_block_t)serviceBlock
priority:(CTBAppServicePriority)priority;
@end
Model層設計
項目中存在很多的模型定義浩销,那組件化后這些模型應該定義在哪呢贯涎?
casatwy
對模型類的觀點是去Model
化,簡單來說就是用字典代替Model
存儲數(shù)據(jù)慢洋。這對于組件化架構來說塘雳,是解決組件之間數(shù)據(jù)傳遞的一個很好的方法。但是去Model
的方式普筹,會存在大量的字段讀取代碼败明,使用起來遠沒有模型類方便。
因為模型類是關乎業(yè)務的太防,理論上必須放在業(yè)務層也就是業(yè)務組件這一層妻顶。但是要把模型對象從一個組件中當做參數(shù)傳遞到另一個組件中,模型類放在調用方和被調方的哪個組件都不太合適蜒车,而且有可能不只兩個組件使用到這個模型對象讳嘱。這樣的話在其他組件使用模型對象,必然會造成引用和耦合酿愧。
如果在用到這個模型對象的所有組件中沥潭,都分別維護一份相同的模型類,或者各自維護不同結構的模型類嬉挡,這樣之后業(yè)務發(fā)生改變模型類就會很麻煩钝鸽,這是不可取的。
設計方案
如果將所有模型類單獨拉出來庞钢,定義一個模型組件呢拔恰?
這個看起來比較可行,將這個定義模型的組件下沉到基礎層基括,模型組件不包含業(yè)務颜懊,只聲明模型對象的類。如果將原來各個組件的模型類定義都拉出來风皿,單獨放在一個組件中饭冬,可以將原有各組件的Model
層變得很輕量,這樣對整個項目架構來說也是有好處的揪阶。
在通過Router
進行組件間調用時,通過字典進行傳值患朱,這種方式比較靈活鲁僚。在組件內部使用Model
層時,還是用模型組件中定義的Model
類。Model
層建議還是用Model
對象的形式比較方便冰沙,不建議整體使用去Model
化的設計侨艾。在接收到其他組件傳遞過來的字典參數(shù)時,可以通過Model
類提供的初始化方法拓挥,或其他轉Model
框架將字典轉為Model
對象唠梨。
@interface CTBStoreWelfareListModel : NSObject
// 自定義初始化方法
- (instancetype)initWithDict:(NSDictionary *)dict;
@end
我公司持久化方案用的是CoreData
,所有模型的定義都在CoreData
組件中侥啤,則不需要再單獨創(chuàng)建一個模型組件当叭。
動態(tài)化構想
我公司項目是一個常規(guī)的地圖類項目,首頁和百度盖灸、高德等主流地圖導航App
一樣蚁鳖,有很多添加在地圖上的控件。有的版本會添加控件上去赁炎,而有的版本會刪除控件醉箕,與之對應的功能也會被隱藏。
所以徙垫,有次和組里小伙伴們開會的時候就在考慮讥裤,能不能在服務器下發(fā)代碼對首頁進行布局!這樣就可以對首頁進行動態(tài)布局姻报,例如有活動的時候在指定時間顯示某個控件己英,這樣可以避免App Store
審核慢的問題。又或者線上某個模塊出現(xiàn)問題逗抑,可以緊急下架出問題的模塊剧辐。
對于這個問題,我們設計了一套動態(tài)配置方案邮府,這套方案可以對整個App
進行配置荧关。
配置表設計
對于動態(tài)配置的問題,我們簡單設計了一個配置表褂傀,初期打算在首頁上先進行試水忍啤,以后可能會布置到更多的頁面上。這樣應用程序各模塊的入口仙辟,都可以通過配置表來控制同波,并且通過Router
控制頁面間跳轉,靈活性非常大叠国。
在第一次安裝程序時使用內置的配置表未檩,之后每次都用服務器來替換本地的配置表,這樣就可以實現(xiàn)動態(tài)配置應用粟焊。下面是一個簡單設計的配置數(shù)據(jù)冤狡,JSON
中配置的是首頁的配置信息孙蒙,用來模擬服務器下發(fā)的數(shù)據(jù),真正服務器下發(fā)的字段會比這個多很多悲雳。
{
"status": 200,
"viewList": [
{
"className": "UIButton",
"frame": {
"originX": 10,
"originY": 10,
"sizeWidth": 50,
"sizeHeight": 30
},
"normalImageURL": "http://image/normal.com",
"highlightedImageURL": "http://image/highlighted.com",
"normalText": "text",
"textColor": "#FFFFFF",
"routerURL": "CTB://search/***"
}
]
}
對于服務器返回的數(shù)據(jù)挎峦,我們會創(chuàng)建一套解析器,這個解析器用來將JSON
解析并“轉換”為標準的UIKit
控件合瓢。點擊后的事件都通過Router
進行跳轉坦胶,所以首頁的靈活性和Router
的使用程度成正比。
這套方案類似于React Native
的方案晴楔,從服務器下發(fā)頁面展示效果顿苇,但沒有React Native
功能那么全。相對而言是一個輕量級的配置方案滥崩,主要用于頁面配置岖圈。
資源動態(tài)配置
除了頁面的配置之外,我們發(fā)現(xiàn)地圖類App
一般都存在ipa
過大的問題钙皮,這樣在下載時很消耗流量以及時間蜂科。所以我們就在想能不能把資源也做到動態(tài)配置,在用戶運行程序的時候再加載資源文件包短条。
我們想通過配置表的方式导匣,將圖片資源文件都放到服務器上,圖片的URL
也隨配置表一起從服務器獲取茸时。在使用時請求圖片并緩存到本地贡定,成為真正的網(wǎng)絡APP
。在此基礎上設計緩存機制可都,定期清理本地的圖片緩存缓待,減少用戶磁盤占用。
滴滴組件化架構
之前看過滴滴iOS
負責人李賢輝的技術分享渠牲,分享的是滴滴iOS
客戶端的架構發(fā)展歷程旋炒,下面簡單總結一下。
發(fā)展歷程
滴滴在最開始的時候架構較混亂签杈。然后在2.0
時期重構為MVC
架構瘫镇,使項目劃分更加清晰。在3.0
時期上線了新的業(yè)務線答姥,這時開始采用游戲開發(fā)中的狀態(tài)機機制铣除,暫時可以滿足現(xiàn)有業(yè)務。
然而在后期不斷上線順風車鹦付、代駕尚粘、巴士等多條業(yè)務線的情況下,現(xiàn)有架構變得非常臃腫敲长,代碼耦合嚴重背苦。從而在2015年開始了代號為“The One”
的方案互捌,這套方案就是滴滴的組件化方案。
架構設計
滴滴的組件化方案行剂,和蘑菇街方案類似,將項目拆分為各個組件钳降,通過CocoaPods
來集成和管理各個組件厚宰。項目被拆分為業(yè)務部分和技術部分,業(yè)務部分包括專車遂填、拼車铲觉、巴士等組件,使用一個pods
管理吓坚。技術部分則分為登錄分享撵幽、網(wǎng)絡、緩存這樣的一些基礎組件礁击,分別使用不同的pods
管理盐杂。
組件間通信通過ONERouter
中間件進行通信,ONERouter
類似于MGJRouter
哆窿,擔負起協(xié)調和調用各個組件的作用链烈。組件間通信通過OpenURL
方法,來進行對應的調用挚躯。ONERouter
內部保存一份Class-URL
的映射表强衡,通過URL
找到Class
并發(fā)起調用,Class
的注冊放在+load
方法中進行码荔。
滴滴在業(yè)務組件內部使用MVVM+MVCS
混合的架構漩勤,兩種架構都是MVC
的衍生版本。其中MVCS
中的Store
負責數(shù)據(jù)相關邏輯缩搅,例如訂單狀態(tài)越败、地址管理等數(shù)據(jù)處理。通過MVVM
中的VM
給控制器瘦身誉己,最后Controller
的代碼量就很少了眉尸。
滴滴首頁分析
滴滴文章中說道首頁只能有一個地圖實例,這在很多地圖導航相關應用中都是這樣做的巨双。滴滴首頁主控制器持有導航欄和地圖噪猾,每個業(yè)務線首頁控制器都添加在主控制器上,并且業(yè)務線控制器背景都設置為透明筑累,將透明部分響應事件傳遞到下面的地圖中袱蜡,只響應屬于自己的響應事件。
由主控制器來切換各個業(yè)務線首頁慢宗,切換頁面后根據(jù)不同的業(yè)務線來更新地圖數(shù)據(jù)坪蚁。
淘寶組件化架構
本章節(jié)源自于宗心在阿里技術沙龍上的一次技術分享
架構發(fā)展
淘寶iOS
客戶端初期是單工程的普通項目奔穿,但隨著業(yè)務的飛速發(fā)展,現(xiàn)有架構并不能承載越來越多的業(yè)務需求敏晤,導致代碼間耦合很嚴重贱田。后期開發(fā)團隊對其不斷進行重構,將項目重構為組件化架構嘴脾,淘寶iOS
和Android
兩個平臺男摧,除了某個平臺特有的一些特性或某些方案不便實施之外,大體架構都是差不多的译打。
發(fā)展歷程
剛開始是普通的單工程項目耗拓,以傳統(tǒng)的
MVC
架構進行開發(fā)。隨著業(yè)務不斷的增加奏司,導致項目非常臃腫乔询、耦合嚴重。2013年淘寶開啟
all in 無線
計劃韵洋,計劃將淘寶變?yōu)橐粋€大的平臺竿刁,將阿里系大多數(shù)業(yè)務都集成到這個平臺上,造成了業(yè)務的大爆發(fā)麻献。
淘寶開始實行插件化架構们妥,將每個業(yè)務模塊劃分為一個子工程,將組件以framework
二方庫的形式集成到主工程勉吻。但這種方式并沒有做到真正的拆分监婶,還是在一個工程中使用git
進行merge
,這樣還會造成合并沖突齿桃、不好回退等問題惑惶。迎來淘寶移動端有史以來最大的重構,將其重構為組件化架構短纵。將每個模塊當做一個組件带污,每個組件都是一個單獨的項目歪泳,并且將組件打包成
framework
港华。主工程通過podfile
集成所有組件的framework
,實現(xiàn)業(yè)務之間真正的隔離任连,通過CocoaPods
實現(xiàn)組件化架構悠就。
架構優(yōu)勢
淘寶是使用git
來做源碼管理的千绪,在插件化架構時需要盡可能避免merge
操作,否則在大團隊中協(xié)作成本是很大的梗脾。而使用CocoaPods
進行組件化開發(fā)荸型,則避免了這個問題。
在CocoaPods
中可以通過podfile
很好的配置各個組件炸茧,包括組件的增加和刪除瑞妇,以及控制某個組件的版本稿静。使用CocoaPods
的原因,很大程度是為了解決大型項目中辕狰,代碼管理工具merge
代碼導致的沖突改备。并且可以通過配置podfile
文件,輕松配置項目柳琢。
每個組件工程有兩個target
绍妨,一個負責編譯當前組件和運行調試,另一個負責打包framework
柬脸。先在組件工程做測試,測試完成后再集成到主工程中集成測試毙驯。
每個組件都是一個獨立app
倒堕,可以獨立開發(fā)、測試爆价,使得業(yè)務組件更加獨立垦巴,所有組件可以并行開發(fā)。下層為上層提供能滿足需求的底層庫铭段,保證上層業(yè)務層可以正常開發(fā)骤宣,并將底層庫封裝成framework
集成到主工程中。
使用CocoaPods
進行組件集成的好處在于序愚,在集成測試自己組件時憔披,可以直接在本地主工程中,通過podfile
使用當前組件源碼爸吮,可以直接進行集成測試芬膝,不需要提交到服務器倉庫。
淘寶四層架構
淘寶架構的核心思想是一切皆組件形娇,將工程中所有代碼都抽象為組件锰霜。
淘寶架構主要分為四層,最上層是組件Bundle
(業(yè)務組件)桐早,依次往下是容器(核心層)癣缅,中間件Bundle
(功能封裝),基礎庫Bundle
(底層庫)哄酝。容器層為整個架構的核心友存,負責組件間的調度和消息派發(fā)。
總線設計
總線設計:URL
路由+服務+消息炫七。統(tǒng)一所有組件的通信標準爬立,各個業(yè)務間通過總線進行通信。
URL總線
通過URL
總線對三端進行了統(tǒng)一万哪,一個URL
可以調起iOS
侠驯、Android
抡秆、前端三個平臺,產品運營和服務器只需要下發(fā)一套URL
即可調用對應的組件吟策。
URL
路由可以發(fā)起請求也可以接受返回值儒士,和MGJRouter
差不多。URL
路由請求可以被解析就直接拿來使用檩坚,如果不能被解析就跳轉H5
頁面着撩。這樣就完成了一個對不存在組件調用的兼容,使用戶手中比較老的版本依然可以顯示新的組件匾委。
服務提供一些公共服務拖叙,由服務方組件負責實現(xiàn),通過Protocol
進行調用赂乐。
消息總線
應用通過消息總線進行事件的中心分發(fā)薯鳍,類似于iOS
的通知機制。例如客戶端前后臺切換挨措,則可以通過消息總線分發(fā)到接收消息的組件挖滤。因為通過URLRouter
只是一對一的進行消息派發(fā)和調度,如果多次注冊同一個URL
浅役,則會被覆蓋掉斩松。
Bundle App
在組件化架構的基礎上,淘寶提出Bundle App
的概念觉既,可以通過已有組件惧盹,進行簡單配置后就可以組成一個新的app
出來。解決了多個應用業(yè)務復用的問題奋救,防止重復開發(fā)同一業(yè)務或功能岭参。
Bundle
即App
,容器即OS
尝艘,所有Bundle App
被集成到OS
上演侯,使每個組件的開發(fā)就像app
開發(fā)一樣簡單。這樣就做到了從巨型app
回歸普通app
的輕盈背亥,使大型項目的開發(fā)問題徹底得到了解決秒际。
總結
各位可以來我博客評論區(qū)討論,可以討論文中提到的技術細節(jié)狡汉,也可以討論自己公司架構所遇到的問題娄徊,或自己獨到的見解等等。無論是不是架構師或新入行的iOS
開發(fā)盾戴,歡迎各位以一個討論技術的心態(tài)來討論寄锐。在評論區(qū)你的問題可以被其他人看到,這樣可能會給其他人帶來一些啟發(fā)。
Demo
地址:蘑菇街和casatwy
組件化方案橄仆,其Github
上都給出了Demo
剩膘,這里就貼出其Github
地址了。
蘑菇街-MGJRouter
casatwy-CTMediator
好多朋友在看完這篇文章后盆顾,都問有沒有Demo
怠褐。其實架構是思想上的東西,重點還是理解架構思想您宪。文章中對思想的概述已經(jīng)很全面了奈懒,用多個項目的例子來描述組件化架構。就算提供了Demo
宪巨,也沒法把Demo
套在其他工程上用磷杏,因為并不一定適合所在的工程。
后來想了一下捏卓,我把組件化架構的集成方式茴丰,簡單寫了個Demo
,這樣可以解決很多人在架構集成上的問題天吓。我把Demo
放在我Github上了,用Coding的服務器來模擬我公司私有服務器峦椰,直接拿MGJRouter
來當Demo
工程中的Router
龄寞。下面是Demo
地址,麻煩各位記得點個star??汤功。
由于簡書排版并不是很好物邑,所以做了一個PDF
版的《組件化架構漫談》,放在我Github
上了滔金。PDF
上有文章目錄色解,方便閱讀,下面是地址餐茵。
如果你覺得不錯科阎,請把PDF
幫忙轉到其他群里,或者你的朋友忿族,讓更多的人了解組件化架構锣笨,衷心感謝!??
Github地址 : https://github.com/DeveloperErenLiu/ComponentArchitectureBook