該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請(qǐng)注明:劉小壯
前段時(shí)間公司項(xiàng)目打算重構(gòu),準(zhǔn)確來說應(yīng)該是按之前的產(chǎn)品邏輯重寫一個(gè)項(xiàng)目??。在重構(gòu)項(xiàng)目之前涉及到架構(gòu)選型的問題,我和組里小伙伴一起研究了一下組件化架構(gòu),打算將項(xiàng)目重構(gòu)為組件化架構(gòu)。當(dāng)然不是直接拿來照搬,還是要根據(jù)公司具體的業(yè)務(wù)需求設(shè)計(jì)架構(gòu)。
在學(xué)習(xí)組件化架構(gòu)的過程中,從很多高質(zhì)量的博客中學(xué)到不少東西,例如蘑菇街李忠、casatwy
、bang
的博客。在學(xué)習(xí)過程中也遇到一些問題,在微博和QQ上和一些做iOS
的朋友進(jìn)行了交流,非常感謝這些朋友的幫助。
本篇文章主要針對(duì)于之前蘑菇街提出的組件化方案,以及casatwy
提出的組件化方案進(jìn)行分析,后面還會(huì)簡(jiǎn)單提到滴滴、淘寶、微信的組件化架構(gòu),最后會(huì)簡(jiǎn)單說一下我公司設(shè)計(jì)的組件化架構(gòu)。
組件化架構(gòu)的由來
隨著移動(dòng)互聯(lián)網(wǎng)的不斷發(fā)展,很多程序代碼量和業(yè)務(wù)越來越多,現(xiàn)有架構(gòu)已經(jīng)不適合公司業(yè)務(wù)的發(fā)展速度了,很多都面臨著重構(gòu)的問題。
在公司項(xiàng)目開發(fā)中,如果項(xiàng)目比較小,普通的單工程+MVC架構(gòu)
就可以滿足大多數(shù)需求了。但是像淘寶、蘑菇街、微信這樣的大型項(xiàng)目,原有的單工程架構(gòu)就不足以滿足架構(gòu)需求了。
就拿淘寶來說,淘寶在13年開啟的“All in 無線”
戰(zhàn)略中,就將阿里系大多數(shù)業(yè)務(wù)都加入到手機(jī)淘寶中,使客戶端出現(xiàn)了業(yè)務(wù)的爆發(fā)。在這種情況下,單工程架構(gòu)則已經(jīng)遠(yuǎn)遠(yuǎn)不能滿足現(xiàn)有業(yè)務(wù)需求了。所以在這種情況下,淘寶在13年開啟了插件化架構(gòu)的重構(gòu),后來在14年迎來了手機(jī)淘寶有史以來最大規(guī)模的重構(gòu),將項(xiàng)目重構(gòu)為組件化架構(gòu)。
蘑菇街的組件化架構(gòu)
原因
在一個(gè)項(xiàng)目越來越大,開發(fā)人員越來越多的情況下,項(xiàng)目會(huì)遇到很多問題。
- 業(yè)務(wù)模塊間劃分不清晰,模塊之間耦合度很大,非常難維護(hù)。
- 所有模塊代碼都編寫在一個(gè)項(xiàng)目中,測(cè)試某個(gè)模塊或功能,需要編譯運(yùn)行整個(gè)項(xiàng)目。
為了解決上面的問題,可以考慮加一個(gè)中間層來協(xié)調(diào)各個(gè)模塊間的調(diào)用,所有的模塊間的調(diào)用都會(huì)經(jīng)過中間層中轉(zhuǎn)。
但是發(fā)現(xiàn)增加這個(gè)中間層后,耦合還是存在的。中間層對(duì)被調(diào)用模塊存在耦合,其他模塊也需要耦合中間層才能發(fā)起調(diào)用。這樣還是存在之前的相互耦合的問題,而且本質(zhì)上比之前更麻煩了。
架構(gòu)改進(jìn)
所以應(yīng)該做的是,只讓其他模塊對(duì)中間層產(chǎn)生耦合關(guān)系,中間層不對(duì)其他模塊發(fā)生耦合。
對(duì)于這個(gè)問題,可以采用組件化的架構(gòu),將每個(gè)模塊作為一個(gè)組件。并且建立一個(gè)主項(xiàng)目,這個(gè)主項(xiàng)目負(fù)責(zé)集成所有組件。這樣帶來的好處是很多的:
- 業(yè)務(wù)劃分更佳清晰,新人接手更佳容易,可以按組件分配開發(fā)任務(wù)。
- 項(xiàng)目可維護(hù)性更強(qiáng),提高開發(fā)效率。
- 更好排查問題,某個(gè)組件出現(xiàn)問題,直接對(duì)組件進(jìn)行處理。
- 開發(fā)測(cè)試過程中,可以只編譯自己那部分代碼,不需要編譯整個(gè)項(xiàng)目代碼。
- 方便集成,項(xiàng)目需要哪個(gè)模塊直接通過
CocoaPods
集成即可。
進(jìn)行組件化開發(fā)后,可以把每個(gè)組件當(dāng)做一個(gè)獨(dú)立的app
,每個(gè)組件甚至可以采取不同的架構(gòu),例如分別使用MVVM
、MVC
、MVCS
等架構(gòu),根據(jù)自己的編程習(xí)慣做選擇。
MGJRouter方案
蘑菇街通過MGJRouter
實(shí)現(xiàn)中間層,由MGJRouter
進(jìn)行組件間的消息轉(zhuǎn)發(fā),從名字上來說更像是“路由器”。實(shí)現(xiàn)方式大致是,在提供服務(wù)的組件中提前注冊(cè)block
,然后在調(diào)用方組件中通過URL
調(diào)用block
,下面是調(diào)用方式。
架構(gòu)設(shè)計(jì)
MGJRouter
是一個(gè)單例對(duì)象,在其內(nèi)部維護(hù)著一個(gè)“URL -> block”
格式的注冊(cè)表,通過這個(gè)注冊(cè)表來保存服務(wù)方注冊(cè)的block
,以及使調(diào)用方可以通過URL
映射出block
,并通過MGJRouter
對(duì)服務(wù)方發(fā)起調(diào)用。
MGJRouter
是所有組件的調(diào)度中心,負(fù)責(zé)所有組件的調(diào)用、切換、特殊處理等操作,可以用來處理一切組件間發(fā)生的關(guān)系。除了原生頁面的解析外,還可以根據(jù)URL
跳轉(zhuǎn)H5
頁面。
在服務(wù)方組件中都對(duì)外提供一個(gè)PublicHeader
,在PublicHeader
中聲明當(dāng)前組件所提供的所有功能,這樣其他組件想知道當(dāng)前組件有什么功能,直接看PublicHeader
即可。每一個(gè)block
都對(duì)應(yīng)著一個(gè)URL
,調(diào)用方可以通過URL
對(duì)block
發(fā)起調(diào)用。
#ifndef UserCenterPublicHeader_h
#define UserCenterPublicHeader_h
// 跳轉(zhuǎn)用戶登錄界面
static const NSString * CTBUCUserLogin = @"CTB://UserCenter/UserLogin";
// 跳轉(zhuǎn)用戶注冊(cè)界面
static const NSString * CTBUCUserRegister = @"CTB://UserCenter/UserRegister";
// 獲取用戶狀態(tài)
static const NSString * CTBUCUserStatus = @"CTB://UserCenter/UserStatus";
#endif
在組件內(nèi)部實(shí)現(xiàn)block
的注冊(cè)工作,以及block
對(duì)外提供服務(wù)的代碼實(shí)現(xiàn)。在注冊(cè)的時(shí)候需要注意注冊(cè)時(shí)機(jī),應(yīng)該保證調(diào)用時(shí)URL
對(duì)應(yīng)的block
已經(jīng)注冊(cè)。
蘑菇街項(xiàng)目使用git
作為版本控制工具,將每個(gè)組件都當(dāng)做一個(gè)獨(dú)立工程,并建立主項(xiàng)目來集成所有組件。集成方式是在主項(xiàng)目中通過CocoaPods
來集成,將所有組件當(dāng)做二方庫
集成到項(xiàng)目中。詳細(xì)的集成技術(shù)點(diǎn)在下面“標(biāo)準(zhǔn)組件化架構(gòu)設(shè)計(jì)”章節(jié)中會(huì)講到。
MGJRouter調(diào)用
下面代碼模擬對(duì)詳情頁的注冊(cè)、調(diào)用,在調(diào)用過程中傳遞id
參數(shù)。參數(shù)傳遞可以有兩種方式,類似于GET
請(qǐng)求在URL
后面拼接參數(shù),以及通過字典傳遞參數(shù)。下面是注冊(cè)的示例代碼:
[MGJRouter registerURLPattern:@"mgj://detail" toHandler:^(NSDictionary *routerParameters) {
// 下面可以在拿到參數(shù)后,為其他組件提供對(duì)應(yīng)的服務(wù)
NSString uid = routerParameters[@"id"];
}];
通過openURL:
方法傳入的URL
參數(shù),對(duì)詳情頁已經(jīng)注冊(cè)的block
方法發(fā)起調(diào)用。調(diào)用方式類似于GET
請(qǐng)求,URL
地址后面拼接參數(shù)。
[MGJRouter openURL:@"mgj://detail?id=404"];
也可以通過字典方式傳參,MGJRouter
提供了帶有字典參數(shù)的方法,這樣就可以傳遞非字符串之外的其他類型參數(shù),例如對(duì)象類型參數(shù)。
[MGJRouter openURL:@"mgj://detail" withParam:@{@"id" : @"404"}];
組件間傳值
有的時(shí)候組件間調(diào)用過程中,需要服務(wù)方在完成調(diào)用后返回相應(yīng)的參數(shù)。蘑菇街提供了另外的方法,專門來完成這個(gè)操作。
[MGJRouter registerURLPattern:@"mgj://cart/ordercount" toObjectHandler:^id(NSDictionary *routerParamters){
return @42;
}];
通過下面的方式發(fā)起調(diào)用,并獲取服務(wù)方返回的返回值,要做的就是傳遞正確的URL
和參數(shù)即可。
NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"];
短鏈管理
這時(shí)候會(huì)發(fā)現(xiàn)一個(gè)問題,在蘑菇街組件化架構(gòu)中,存在了很多硬編碼的URL
和參數(shù)。在代碼實(shí)現(xiàn)過程中URL
編寫出錯(cuò)會(huì)導(dǎo)致調(diào)用失敗,而且參數(shù)是一個(gè)字典類型,調(diào)用方不知道服務(wù)方需要哪些參數(shù),這些都是個(gè)問題。
對(duì)于這些數(shù)據(jù)的管理,蘑菇街開發(fā)了一個(gè)web
頁面,這個(gè)web
頁面統(tǒng)一來管理所有的URL
和參數(shù),Android
和iOS
都使用這一套URL
,可以保持統(tǒng)一性。
基礎(chǔ)組件
在項(xiàng)目中存在很多公共部分的東西,例如封裝的網(wǎng)絡(luò)請(qǐng)求、緩存、數(shù)據(jù)處理等功能,以及項(xiàng)目中所用到的資源文件。蘑菇街將這些部分也當(dāng)做組件,劃分為基礎(chǔ)組件,位于業(yè)務(wù)組件下層。所有業(yè)務(wù)組件都使用同一套基礎(chǔ)組件,也可以保證公共部分的統(tǒng)一性。
Protocol方案
整體架構(gòu)
為了解決MGJRouter
方案中URL
硬編碼,以及字典參數(shù)類型不明確等問題,蘑菇街在原有組件化方案的基礎(chǔ)上推出了Protocol
方案。Protocol
方案由兩部分組成,進(jìn)行組件間通信的ModuleManager
類以及MGJComponentProtocol
協(xié)議類。
通過中間件ModuleManager
進(jìn)行消息的調(diào)用轉(zhuǎn)發(fā),在ModuleManager
內(nèi)部維護(hù)一張映射表,映射表由之前的"URL -> block"
變成"Protocol -> Class"
。
在中間件中創(chuàng)建MGJComponentProtocol
文件,服務(wù)方組件將可以用來調(diào)用的方法都定義在Protocol
中,將所有服務(wù)方的Protocol
都分別定義到MGJComponentProtocol
文件中,如果協(xié)議比較多也可以分開幾個(gè)文件定義。這樣所有調(diào)用方依然是只依賴中間件,不需要依賴除中間件之外的其他組件。
Protocol
方案中每個(gè)組件需要一個(gè)MGJModuleImplement
,此類負(fù)責(zé)實(shí)現(xiàn)當(dāng)前組件對(duì)應(yīng)的協(xié)議方法,也就是對(duì)外提供服務(wù)的實(shí)現(xiàn)。在程序開始運(yùn)行時(shí)將自身的Class
注冊(cè)到ModuleManager
中,并將Protocol
反射為字符串當(dāng)做key
。
Protocol
方案依然需要提前注冊(cè)服務(wù),由于Protocol
方案是返回一個(gè)Class
,并將Class
反射為對(duì)象再調(diào)用方法,這種方式不會(huì)直接調(diào)用類的內(nèi)部邏輯??梢詫?code>Protocol方案的Class
注冊(cè),都放在類對(duì)應(yīng)的MGJModuleImplement
中,或者專門建立一個(gè)RegisterProtocol
類。
示例代碼
創(chuàng)建MGJUserImpl
類當(dāng)做User
組件對(duì)外公開的類,并在MGJComponentProtocol.h
中定義MGJUserProtocol
協(xié)議,由MGJUserImpl
類實(shí)現(xiàn)協(xié)議中定義的方法,完成對(duì)外提供服務(wù)的過程。下面是協(xié)議定義:
@protocol MGJUserProtocol <NSObject>
- (NSString *)getUserName;
@end
Class
遵守協(xié)議并實(shí)現(xiàn)定義的方法,外界通過Protocol
獲取的Class
并實(shí)例化為對(duì)象,調(diào)用服務(wù)方實(shí)現(xiàn)的協(xié)議方法。
ModuleManager
的協(xié)議注冊(cè)方法,注冊(cè)時(shí)將Protocol
反射為字符串當(dāng)做存儲(chǔ)的key
,將實(shí)現(xiàn)協(xié)議的Class
當(dāng)做值存儲(chǔ)。通過Protocol
取Class
的時(shí)候,就是通過Protocol
從ModuleManager
中將Class
映射出來。
[ModuleManager registerClass:MGJUserImpl forProtocol:@protocol(MGJUserProtocol)];
調(diào)用時(shí)通過Protocol
從ModuleManager
中映射出注冊(cè)的Class
,將獲取到的Class
實(shí)例化,并調(diào)用Class
實(shí)現(xiàn)的協(xié)議方法完成服務(wù)調(diào)用。
Class cls = [[ModuleManager sharedInstance] classForProtocol:@protocol(MGJUserProtocol)];
id userComponent = [[cls alloc] init];
NSString *userName = [userComponent getUserName];
項(xiàng)目調(diào)用流程
蘑菇街是MGJRouter
和Protocol
混用的方式,兩種實(shí)現(xiàn)的調(diào)用方式不同,但大體調(diào)用邏輯和實(shí)現(xiàn)思路類似。在MGJRouter
不能滿足需求或調(diào)用不方便時(shí),就可以通過Protocol
的方式調(diào)用。
在進(jìn)入程序后,先使用
MGJRouter
對(duì)服務(wù)方組件進(jìn)行注冊(cè)。每個(gè)URL
對(duì)應(yīng)一個(gè)block
的實(shí)現(xiàn),block
中的代碼就是組件對(duì)外提供的服務(wù),調(diào)用方可以通過URL
調(diào)用這個(gè)服務(wù)。調(diào)用方通過
MGJRouter
調(diào)用openURL:
方法,并將被調(diào)用代碼對(duì)應(yīng)的URL
傳入,MGJRouter
會(huì)根據(jù)URL
查找對(duì)應(yīng)的block
實(shí)現(xiàn),從而調(diào)用組件的代碼進(jìn)行通信。調(diào)用和注冊(cè)
block
時(shí),block
有一個(gè)字典用來傳遞參數(shù)。這樣的優(yōu)勢(shì)就是參數(shù)類型和數(shù)量理論上是不受限制的,但是需要很多硬編碼的key
名在項(xiàng)目中。
內(nèi)存管理
蘑菇街組件化方案有兩種,Protocol
和MGJRouter
的方式,但都需要進(jìn)行register
操作。Protocol
注冊(cè)的是Class
,MGJRouter
注冊(cè)的是Block
,注冊(cè)表是一個(gè)NSMutableDictionary
類型的字典,而字典的擁有者又是一個(gè)單例對(duì)象,這樣會(huì)造成內(nèi)存的常駐。
下面是對(duì)兩種實(shí)現(xiàn)方式內(nèi)存消耗的分析:
首先說一下
MGJRouter
方案可能導(dǎo)致的內(nèi)存問題,由于block
會(huì)對(duì)代碼塊內(nèi)部對(duì)象進(jìn)行持有,如果使用不當(dāng)很容易造成內(nèi)存泄漏的問題。
block
自身實(shí)際上不會(huì)造成很大的內(nèi)存泄漏,主要是內(nèi)部引用的變量,所以在使用時(shí)就需要注意強(qiáng)引用的問題,并適當(dāng)使用weak
修飾對(duì)應(yīng)的變量。以及在適當(dāng)?shù)臅r(shí)候,釋放對(duì)應(yīng)的變量。
除了對(duì)外部變量的引用,在block
代碼塊內(nèi)部盡量不要直接創(chuàng)建對(duì)象,應(yīng)該通過方法調(diào)用中轉(zhuǎn)一下。對(duì)于協(xié)議這種實(shí)現(xiàn)方式,和
block
內(nèi)存常駐方式差不多。只是將存儲(chǔ)的block
對(duì)象換成Class
對(duì)象。這實(shí)際上是存儲(chǔ)的類對(duì)象,類對(duì)象本來就是單例模式,所以不會(huì)造成多余內(nèi)存占用。
casatwy組件化方案
整體架構(gòu)
casatwy
組件化方案可以處理兩種方式的調(diào)用,遠(yuǎn)程調(diào)用和本地調(diào)用,對(duì)于兩個(gè)不同的調(diào)用方式分別對(duì)應(yīng)兩個(gè)接口。
遠(yuǎn)程調(diào)用通過
AppDelegate
代理方法傳遞到當(dāng)前應(yīng)用后,調(diào)用遠(yuǎn)程接口并在內(nèi)部做一些處理,處理完成后會(huì)在遠(yuǎn)程接口內(nèi)部調(diào)用本地接口,以實(shí)現(xiàn)本地調(diào)用為遠(yuǎn)程調(diào)用服務(wù)。本地調(diào)用由
performTarget:action:params:
方法負(fù)責(zé),但調(diào)用方一般不直接調(diào)用performTarget:
方法。CTMediator
會(huì)對(duì)外提供明確參數(shù)和方法名的方法,在方法內(nèi)部調(diào)用performTarget:
方法和參數(shù)的轉(zhuǎn)換。
架構(gòu)設(shè)計(jì)思路
casatwy
是通過CTMediator
類實(shí)現(xiàn)組件化的,在此類中對(duì)外提供明確參數(shù)類型的接口,接口內(nèi)部通過performTarget
方法調(diào)用服務(wù)方組件的Target
、Action
。由于CTMediator
類的調(diào)用是通過runtime
主動(dòng)發(fā)現(xiàn)服務(wù)的,所以服務(wù)方對(duì)此類是完全解耦的。
但如果CTMediator
類對(duì)外提供的方法都放在此類中,將會(huì)對(duì)CTMediator
造成極大的負(fù)擔(dān)和代碼量。解決方法就是對(duì)每個(gè)服務(wù)方組件創(chuàng)建一個(gè)CTMediator
的Category
,并將對(duì)服務(wù)方的performTarget
調(diào)用放在對(duì)應(yīng)的Category
中,這些Category
都屬于CTMediator
中間件,從而實(shí)現(xiàn)了感官上的接口分離。
對(duì)于服務(wù)方的組件來說,每個(gè)組件都提供一個(gè)或多個(gè)Target
類,在Target
類中聲明Action
方法。Target
類是當(dāng)前組件對(duì)外提供的一個(gè)“服務(wù)類”,Target
將當(dāng)前組件中所有的服務(wù)都定義在里面,CTMediator
通過runtime
主動(dòng)發(fā)現(xiàn)服務(wù)。
在Target
中的所有Action
方法,都只有一個(gè)字典參數(shù),所以可以傳遞的參數(shù)很靈活,這也是casatwy
提出的去Model
化的概念。在Action
的方法實(shí)現(xiàn)中,對(duì)傳進(jìn)來的字典參數(shù)進(jìn)行解析,再調(diào)用組件內(nèi)部的類和方法。
架構(gòu)分析
casatwy
為我們提供了一個(gè)Demo,通過這個(gè)Demo
可以很好的理解casatwy
的設(shè)計(jì)思路,下面按照我的理解講解一下這個(gè)Demo
。
打開Demo
后可以看到文件目錄非常清楚,在上圖中用藍(lán)框框出來的就是中間件部分,紅框框出來的就是業(yè)務(wù)組件部分。我對(duì)每個(gè)文件夾做了一個(gè)簡(jiǎn)單的注釋,包含了其在架構(gòu)中的職責(zé)。
在CTMediator
中定義遠(yuǎn)程調(diào)用和本地調(diào)用的兩個(gè)方法,其他業(yè)務(wù)相關(guān)的調(diào)用由Category
完成。
// 遠(yuǎn)程App調(diào)用入口
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;
// 本地組件調(diào)用入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;
在CTMediator
中定義的ModuleA
的Category
,為其他組件提供了一個(gè)獲取控制器并跳轉(zhuǎn)的功能,下面是代碼實(shí)現(xiàn)。由于casatwy
的方案中使用performTarget
的方式進(jìn)行調(diào)用,所以涉及到很多硬編碼字符串的問題,casatwy
采取定義常量字符串來解決這個(gè)問題,這樣管理也更方便。
#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 {
// 這里處理異常場(chǎng)景,具體如何處理取決于產(chǎn)品邏輯
return [[UIViewController alloc] init];
}
}
下面是ModuleA
組件中提供的服務(wù),被定義在Target_A
類中,這些服務(wù)可以被CTMediator
通過runtime
的方式調(diào)用,這個(gè)過程就叫做發(fā)現(xiàn)服務(wù)。
在Target_A
中對(duì)傳遞的參數(shù)做了處理,以及內(nèi)部的業(yè)務(wù)邏輯實(shí)現(xiàn)。方法是發(fā)生在ModuleA
內(nèi)部的,這樣就可以保證組件內(nèi)部的業(yè)務(wù)不受外部影響,對(duì)內(nèi)部業(yè)務(wù)沒有侵入性。
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params {
// 對(duì)傳過來的字典參數(shù)進(jìn)行解析,并調(diào)用ModuleA內(nèi)部的代碼
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}
命名規(guī)范
在大型項(xiàng)目中代碼量比較大,需要避免命名沖突的問題。對(duì)于這個(gè)問題casatwy
采取的是加前綴的方式,從casatwy
的Demo
中也可以看出,其組件ModuleA
的Target
命名為Target_A
,可以區(qū)分各個(gè)組件的Target
。被調(diào)用的Action
命名為Action_nativeFetchDetailViewController:
,可以區(qū)分組件內(nèi)的方法與對(duì)外提供的方法。
casatwy
將類和方法的命名,都統(tǒng)一按照其功能做區(qū)分當(dāng)做前綴,這樣很好的將組件相關(guān)和組件內(nèi)部代碼進(jìn)行了劃分。
結(jié)果分析
Protocol
從我調(diào)研和使用的結(jié)果來說,并不推薦使用Protocol
方案。首先Protocol
方案的代碼量就比MGJRouter
方案的要多,調(diào)用和注冊(cè)代碼量很大,調(diào)用起來并不是很方便。
本質(zhì)上來說Protocol
方案是通過類對(duì)象實(shí)例一個(gè)變量,并調(diào)用變量的方法,并沒有真正意義上的改變組件之間的交互方案,但MGJRouter
的方案卻通過URL Router
的方式改變和統(tǒng)一了組件間調(diào)用方式。
并且Protocol
沒有對(duì)Remote Router
的支持,不能直接處理來自Push
的調(diào)用,在靈活性上就不如MGJRouter
的方案。
CTMediator
我并不推薦CTMediator
方案,這套方案實(shí)際上是一套很臃腫的方案。雖然為CTMediator
提供了很多Category
,但實(shí)際上組件間的調(diào)用邏輯都耦合在了中間件中。同樣,和Protocol
方案存在一個(gè)相同的問題,就是調(diào)用代碼量很大,使用起來并不方便。
在CTMediator
方案中存在很多硬編碼的問題,例如target
、action
以及參數(shù)名都是硬編碼在中間件中的,這種調(diào)用方式并不靈活直接。
但casatwy
提出了去Model
化的想法,我覺得這在組件化中傳參來說,是非常靈活的,這點(diǎn)我比較認(rèn)同。相對(duì)于MGJRouter
的話,也采用了去Model
化的傳參方式,而不是直接傳遞模型對(duì)象。組件化傳參并不適用傳模型對(duì)象,但組件內(nèi)部還是可以使用Model
的。
MGJRouter
MGJRouter
方案是一套非常輕量級(jí)的方案,其中間件代碼總共也就兩百行以內(nèi),非常簡(jiǎn)潔。在調(diào)用時(shí)直接通過URL
調(diào)用,調(diào)用起來很簡(jiǎn)單,我推薦使用這套方案作為組件化架構(gòu)的中間件。
MGJRouter
最強(qiáng)大的一點(diǎn)在于,統(tǒng)一了遠(yuǎn)程調(diào)用和本地調(diào)用。這就使得可以通過Push
的方式,進(jìn)行任何允許的組件間調(diào)用,對(duì)項(xiàng)目運(yùn)營(yíng)是有很大幫助的。
這三套方案都實(shí)現(xiàn)了組件間的解耦,MGJRouter
和Protocol
都是調(diào)用方對(duì)中間件的耦合,CTMediator
是中間件對(duì)組件的耦合,都是單向耦合。
接口類
在三套方案中,服務(wù)方組件都對(duì)外提供一個(gè)PublicHeader
或Target
,在文件中統(tǒng)一定義對(duì)外提供的服務(wù),組件間通信的實(shí)現(xiàn)代碼大多數(shù)都在里面。
但三套實(shí)現(xiàn)方案實(shí)現(xiàn)方式并不同,蘑菇街的兩套方案都需要注冊(cè)操作,無論是Block
還是Protocol
都需要注冊(cè)后才可以提供服務(wù)。而casatwy
的方案則不需要,直接通過runtime
調(diào)用。
組件化架構(gòu)設(shè)計(jì)
在上面文章中提到了casatwy
方案的CTMediator
,蘑菇街方案的MGJRouter
和ModuleManager
,之后將統(tǒng)稱為中間件,下面讓我們?cè)O(shè)計(jì)一套組件化架構(gòu)。
整體架構(gòu)
組件化架構(gòu)中,需要一個(gè)主工程,主工程負(fù)責(zé)集成所有組件。每個(gè)組件都是一個(gè)單獨(dú)的工程,創(chuàng)建不同的git
私有倉庫來管理,每個(gè)組件都有對(duì)應(yīng)的開發(fā)人員負(fù)責(zé)開發(fā)。開發(fā)人員只需要關(guān)注與其相關(guān)組件的代碼,不用考慮其他組件,這樣來新人也好上手。
組件的劃分需要注意組件粒度,粒度根據(jù)業(yè)務(wù)可大可小。組件劃分可以將每個(gè)業(yè)務(wù)模塊都劃分為組件,對(duì)于網(wǎng)絡(luò)、數(shù)據(jù)庫等基礎(chǔ)模塊,也應(yīng)該劃分到組件中。項(xiàng)目中會(huì)用到很多資源文件、配置文件等,也應(yīng)該劃分到對(duì)應(yīng)的組件中,避免重復(fù)的資源文件。項(xiàng)目實(shí)現(xiàn)完全的組件化。
每個(gè)組件都需要對(duì)外提供調(diào)用,在對(duì)外公開的類或組件內(nèi)部,注冊(cè)對(duì)應(yīng)的URL
。組件處理中間件調(diào)用的代碼應(yīng)該對(duì)其他代碼無侵入,只負(fù)責(zé)對(duì)傳遞過來的數(shù)據(jù)進(jìn)行解析和組件內(nèi)調(diào)用的功能。
組件集成
每個(gè)組件都是一個(gè)單獨(dú)的工程,在組件開發(fā)完成后上傳到git
倉庫。主工程通過Cocoapods
集成各個(gè)組件,集成和更新組件時(shí)只需要pod update
即可。這樣就是把每個(gè)組件當(dāng)做第三方來管理,管理起來非常方便。
Cocoapods
可以控制每個(gè)組件的版本,例如在主項(xiàng)目中回滾某個(gè)組件到特定版本,就可以通過修改podfile
文件實(shí)現(xiàn)。選擇Cocoapods
主要因?yàn)槠浔旧砉δ芎軓?qiáng)大,可以很方便的集成整個(gè)項(xiàng)目,也有利于代碼的復(fù)用。通過這種集成方式,可以很好的避免在傳統(tǒng)項(xiàng)目中代碼沖突的問題。
集成方式
對(duì)于組件化架構(gòu)的集成方式,我在看完bang
的博客后專門請(qǐng)教了一下bang
。根據(jù)在微博上和bang
的聊天以及其他博客中的學(xué)習(xí),在主項(xiàng)目中集成組件主要分為兩種方式——源碼和framework
,但都是通過CocoaPods
來集成。
無論是用CocoaPods
管理源碼,還是直接管理framework
,集成方式都是一樣的,都是直接進(jìn)行pod update
等CocoaPods
操作。
這兩種組件集成方案,實(shí)踐中也是各有利弊。直接在主工程中集成代碼文件,可以看到其內(nèi)部實(shí)現(xiàn)源碼,方便在主工程中進(jìn)行調(diào)試。集成framework
的方式,可以加快編譯速度,而且對(duì)每個(gè)組件的代碼有很好的保密性。如果公司對(duì)代碼安全比較看重,可以考慮framework
的形式。
例如手機(jī)QQ或者支付寶這樣的大型程序,一般都會(huì)采取framework
的形式。而且一般這樣的大公司,都會(huì)有自己的組件庫,這個(gè)組件庫往往可以代表一個(gè)大的功能或業(yè)務(wù)組件,直接添加項(xiàng)目中就可以使用。關(guān)于組件化庫在后面講淘寶組件化架構(gòu)的時(shí)候會(huì)提到。
資源文件
對(duì)于項(xiàng)目中圖片的集成,可以把圖片當(dāng)做一個(gè)單獨(dú)的組件,組件中只存在圖片文件,沒有任何代碼。圖片可以使用Bundle
和image assets
進(jìn)行管理,如果是Bundle
就針對(duì)不同業(yè)務(wù)模塊建立不同的Bundle
,如果是image assets
,就按照不同的模塊分類建立不同的assets
,將所有資源放在同一個(gè)組件內(nèi)。
Bundle
和image assets
兩者相比,我還是更推薦用assets
的方式,因?yàn)?code>assets自身提供很多功能(例如設(shè)置圖片拉伸范圍),而且在打包之后圖片會(huì)被打包在.cer
文件中,不會(huì)被看到。(現(xiàn)在也可以通過工具對(duì).cer
文件進(jìn)行解析,獲取里面的圖片)
使用Cocoapods
,所有的資源文件都放置在一個(gè)podspec
中,主工程可以直接引用這個(gè)podspec
,假設(shè)此podspec
名為:Assets
,而這個(gè)Assets
的podspec
里面配置信息可以寫為:
s.resources = "Assets/Assets.xcassets/ ** / *.{png}"
主工程則直接在podfile
文件中加入:
pod 'Assets', :path => '../MainProject/Assets'(這種寫法是訪問本地的,可以換成git)
這樣即可在主工程直接訪問到Assets
中的資源文件(不局限圖片,sqlite
、js
、html
亦可,在s.resources
設(shè)置好配置信息即可)了。
優(yōu)點(diǎn)
組件化開發(fā)可以很好的提升代碼復(fù)用性,組件可以直接拿到其他項(xiàng)目中使用,這個(gè)優(yōu)點(diǎn)在下面淘寶架構(gòu)中會(huì)著重講一下。
對(duì)于調(diào)試工作,可以放在每個(gè)組件中完成。單獨(dú)的業(yè)務(wù)組件可以直接提交給測(cè)試使用,這樣測(cè)試起來也比較方便。最后組件開發(fā)完成并測(cè)試通過后,再將所有組件更新到主項(xiàng)目,提交給測(cè)試進(jìn)行集成測(cè)試即可。
通過這樣的組件劃分,組件的開發(fā)進(jìn)度不會(huì)受其他業(yè)務(wù)的影響,可以多個(gè)組件并行開發(fā)。組件間的通信都交給中間件來進(jìn)行,需要通信的類只需要接觸中間件,而中間件不需要耦合其他組件,這就實(shí)現(xiàn)了組件間的解耦。中間件負(fù)責(zé)處理所有組件之間的調(diào)度,在所有組件之間起到控制核心的作用。
組件化框架清晰的劃分了不同模塊,從整體架構(gòu)上來約束開發(fā)人員進(jìn)行組件化開發(fā),實(shí)現(xiàn)了組件間的物理隔離。組件化架構(gòu)在各個(gè)模塊之間天然形成了一道屏障,避免某個(gè)開發(fā)人員偷懶直接引用頭文件,產(chǎn)生組件間的耦合,破壞整體架構(gòu)。
使用組件化架構(gòu)進(jìn)行開發(fā)時(shí),因?yàn)槊總€(gè)人都負(fù)責(zé)自己的組件,代碼提交也只提交自己負(fù)責(zé)模塊的倉庫,所以代碼沖突的問題會(huì)變得很少。
假設(shè)以后某個(gè)業(yè)務(wù)發(fā)生大的改變,需要對(duì)相關(guān)代碼進(jìn)行重構(gòu),可以在單個(gè)組件內(nèi)進(jìn)行重構(gòu)。組件化架構(gòu)降低了重構(gòu)的風(fēng)險(xiǎn),保證了代碼的健壯性。
架構(gòu)分析
在MGJRouter
方案中,是通過調(diào)用OpenURL:
方法并傳入URL
來發(fā)起調(diào)用的。鑒于URL
協(xié)議名等固定格式,可以通過判斷協(xié)議名的方式,使用配置表控制H5
和native
的切換,配置表可以從后臺(tái)更新,只需要將協(xié)議名更改一下即可。
mgj://detail?id=123456
http://www.mogujie.com/detail?id=123456
假設(shè)現(xiàn)在線上的native
組件出現(xiàn)嚴(yán)重bug
,在后臺(tái)將配置文件中原有的本地URL
換成H5
的URL
,并更新客戶端配置文件。
在調(diào)用MGJRouter
時(shí)傳入這個(gè)H5
的URL
即可完成切換,MGJRouter
判斷如果傳進(jìn)來的是一個(gè)H5
的URL
就直接跳轉(zhuǎn)webView
。而且URL
可以傳遞參數(shù)給MGJRouter
,只需要MGJRouter
內(nèi)部做參數(shù)截取即可。
使用組件化架構(gòu)開發(fā),組件間的通信都是有成本的。所以盡量將業(yè)務(wù)封裝在組件內(nèi)部,對(duì)外只提供簡(jiǎn)單的接口。即“高內(nèi)聚、低耦合”原則。
把握好組件劃分粒度的細(xì)化程度,太細(xì)則項(xiàng)目過于分散,太大則項(xiàng)目組件臃腫。但是項(xiàng)目都是從小到大的一個(gè)發(fā)展過程,所以不斷進(jìn)行重構(gòu)是掌握這個(gè)組件的細(xì)化程度最好的方式。
注意點(diǎn)
如果通過framework
等二進(jìn)制形式,將組件集成到主項(xiàng)目中,需要注意預(yù)編譯指令的使用。因?yàn)轭A(yù)編譯指令在打包framework
的時(shí)候,就已經(jīng)在組件二進(jìn)制代碼中打包好,到主項(xiàng)目中的時(shí)候預(yù)編譯指令其實(shí)已經(jīng)不再起作用了,而是已經(jīng)在打包時(shí)按照預(yù)編譯指令編碼為固定二進(jìn)制。
我公司架構(gòu)
對(duì)于項(xiàng)目架構(gòu)來說,一定要建立于業(yè)務(wù)之上來設(shè)計(jì)架構(gòu)。不同的項(xiàng)目業(yè)務(wù)不同,組件化方案的設(shè)計(jì)也會(huì)不同,應(yīng)該設(shè)計(jì)最適合公司業(yè)務(wù)的架構(gòu)。
架構(gòu)設(shè)計(jì)
我公司項(xiàng)目是一個(gè)地圖導(dǎo)航應(yīng)用,業(yè)務(wù)層之下的核心模塊和基礎(chǔ)模塊占比較大,涉及到地圖SDK、算路、語音等模塊。且基礎(chǔ)模塊相對(duì)比較獨(dú)立,對(duì)外提供了很多調(diào)用接口。由此可以看出,公司項(xiàng)目是一個(gè)重邏輯的項(xiàng)目,不像電商等App
偏展示。
項(xiàng)目整體的架構(gòu)設(shè)計(jì)是:層級(jí)架構(gòu)+組件化架構(gòu),對(duì)于具體的實(shí)現(xiàn)細(xì)節(jié)會(huì)在下面詳細(xì)講解。采取這種結(jié)構(gòu)混合的方式進(jìn)行整體架構(gòu),對(duì)于組件的管理和層級(jí)劃分比較有利,符合公司業(yè)務(wù)需求。
在設(shè)計(jì)架構(gòu)時(shí),我們將整個(gè)項(xiàng)目都拆分為組件,組件化程度相當(dāng)高。用到哪個(gè)組件就在工程中通過Podfile
進(jìn)行集成,并通過URLRouter
統(tǒng)一所有組件間的通信。
組件化架構(gòu)是項(xiàng)目的整體框架,而對(duì)于框架中每個(gè)業(yè)務(wù)模塊的實(shí)現(xiàn),可以是任意方式的架構(gòu),MVVM
、MVC
、MVCS
等都是可以的,只要通過MGJRouter
將組件間的通信方式統(tǒng)一即可。
分層架構(gòu)
組件化架構(gòu)在物理結(jié)構(gòu)上來說是不分層次的,只有組件與組件之間的劃分關(guān)系。但是在組件化架構(gòu)的基礎(chǔ)上,應(yīng)該根據(jù)項(xiàng)目和業(yè)務(wù)設(shè)計(jì)自己的層次架構(gòu),這套層次架構(gòu)可以用來區(qū)分組件所處的層次及職責(zé),所以我們?cè)O(shè)計(jì)了層級(jí)架構(gòu)+組件化架構(gòu)的整體架構(gòu)。
我公司項(xiàng)目最開始設(shè)計(jì)的是三層架構(gòu):業(yè)務(wù)層 -> 核心層 (high + low) -> 基礎(chǔ)層
,其中核心層又分為high
和low
兩部分。但是這種架構(gòu)會(huì)造成核心層過重,基礎(chǔ)層過輕的問題,這種并不適合組件化架構(gòu)。
在三層架構(gòu)中會(huì)發(fā)現(xiàn),low
層并沒有耦合業(yè)務(wù)邏輯,在同層級(jí)中是比較獨(dú)立的,職責(zé)較為單一和基礎(chǔ)。我們對(duì)low
層下沉到基礎(chǔ)層中,并和基礎(chǔ)層進(jìn)行合并。所以架構(gòu)被重新分為三層架構(gòu):業(yè)務(wù)層 -> 核心層 -> 基礎(chǔ)層
。之前基礎(chǔ)層大多是資源文件和配置文件,在項(xiàng)目中存在感并不高。
在分層架構(gòu)中,需要注意只能上層對(duì)下層依賴,下層對(duì)上層不能有依賴,下層中不要包含上層業(yè)務(wù)邏輯。對(duì)于項(xiàng)目中存在的公共資源和代碼,應(yīng)該將其下沉到下層中。
職責(zé)劃分
在三層架構(gòu)中,業(yè)務(wù)層負(fù)責(zé)處理上層業(yè)務(wù),將不同業(yè)務(wù)劃分到相應(yīng)組件中,例如IM
組件、導(dǎo)航組件、用戶組件等。業(yè)務(wù)層的組件間關(guān)系比較復(fù)雜,會(huì)涉及到組件間業(yè)務(wù)的通信,以及業(yè)務(wù)層組件對(duì)下層組件的引用。
核心層位于業(yè)務(wù)層下方,為業(yè)務(wù)層提供業(yè)務(wù)支持,如網(wǎng)絡(luò)、語音識(shí)別等組件應(yīng)該劃分到核心層。核心層應(yīng)該盡量減少組件間的依賴,將依賴降到最小。核心層有時(shí)相互之間也需要支持,例如經(jīng)緯度組件需要網(wǎng)絡(luò)組件提供網(wǎng)絡(luò)請(qǐng)求的支持,這種是不可避免的。
其他比較基礎(chǔ)的模塊,都放在基礎(chǔ)層當(dāng)做基礎(chǔ)組件。例如AFN
、地圖SDK
、加密算法等,這些組件都比較獨(dú)立且不摻雜任何業(yè)務(wù)邏輯,職責(zé)更加單一,相對(duì)于核心層更底層。可以包含第三方庫、資源文件、配置文件、基礎(chǔ)庫等幾大類,基礎(chǔ)層組件相互之間不應(yīng)該產(chǎn)生任何依賴。
在設(shè)計(jì)各個(gè)組件時(shí),應(yīng)該遵循“高內(nèi)聚,低耦合”的設(shè)計(jì)規(guī)范,組件的調(diào)用應(yīng)該簡(jiǎn)單且直接,減少調(diào)用方的其他處理。對(duì)于核心層和基礎(chǔ)層的劃分,可以以是否涉及業(yè)務(wù)、是否涉及同級(jí)組件間通信、是否經(jīng)常改動(dòng)為參照點(diǎn)。如果符合這幾點(diǎn)則放在核心層,如果不符合則放在基礎(chǔ)層。
集成方式
新建一個(gè)項(xiàng)目后,首先將配置文件、URLRouter
、App
容器等集成到主工程中,做一些基礎(chǔ)的項(xiàng)目配置,隨后集成需要的組件即可。項(xiàng)目被整體拆分為組件化架構(gòu)后,應(yīng)用對(duì)所有組件的集成方式都是一樣的,通過Podfile
將需要的組件集成到項(xiàng)目中。通過組件化的方式,使得開發(fā)新項(xiàng)目速度變得非???。
在集成業(yè)務(wù)層和核心層組件后,組件間的通信都是由URLRouter
進(jìn)行通信,項(xiàng)目中不允許直接依賴組件源碼。而基礎(chǔ)層組件則在集成后直接依賴,例如資源文件和配置文件,這些都是直接在主工程或組件中使用的。第三方庫則是通過核心層的業(yè)務(wù)封裝,封裝后由URLRouter
進(jìn)行通信,但核心層也是直接依賴第三方庫源碼的。
組件的集成方式有兩種,源碼和framework
的形式,我們使用framework
的方式集成。因?yàn)橐话愣际琼?xiàng)目比較大才用組件化的,但大型項(xiàng)目都會(huì)存在編譯時(shí)間的問題,如果通過framework
則會(huì)大大減少編譯時(shí)間,可以節(jié)省開發(fā)人員的時(shí)間。
組件間通信
對(duì)于組件間通信,我們采用的MGJRouter
方案。因?yàn)?code>MGJRouter現(xiàn)在已經(jīng)很穩(wěn)定了,而且可以滿足蘑菇街這樣量級(jí)的App
需求,證明是很好的,沒必要自己寫一套再慢慢踩坑。
MGJRouter
的好處在于,其調(diào)用方式很靈活,通過MGJRouter
注冊(cè)并在block
中處理回調(diào),通過URL
直接調(diào)用或者URL+Params
字典的方式進(jìn)行調(diào)用。由于通過URL
拼接參數(shù)或Params
字典傳值,所以其參數(shù)類型沒有數(shù)量限定,傳遞比較靈活。在通過openURL:
調(diào)用后,可以在completionBlock
中處理完成邏輯。
MGJRouter
有個(gè)問題在于,在編寫組件間通信的代碼時(shí),會(huì)涉及到大量的Hardcode
。對(duì)于Hardcode
的問題,蘑菇街開發(fā)了一套后臺(tái)系統(tǒng),將所有的Router
需要的URL
和參數(shù)名,都定義到這套系統(tǒng)中。我們維護(hù)了一個(gè)Plist
表,內(nèi)部按不同組件進(jìn)行劃分,包含URL
和傳參名以及回調(diào)參數(shù)。
路由層安全
組件化架構(gòu)需要注意路由層的安全問題。MGJRouter
方案可以處理本地及遠(yuǎn)程的OpenURL
調(diào)用,如果是程序內(nèi)組件間的OpenURL
調(diào)用,則不需要進(jìn)行校驗(yàn)。而跨應(yīng)用的OpenURL
調(diào)用,則需要進(jìn)行合法性檢查。這是為了防止第三方偽造進(jìn)行OpenURL
調(diào)用,所以對(duì)應(yīng)用外調(diào)起的OpenURL
進(jìn)行的合法性檢查,例如其他應(yīng)用調(diào)起、服務(wù)器Remote Push
等。
在合法性檢查的設(shè)計(jì)上,每個(gè)從應(yīng)用外調(diào)起的合法URL
都會(huì)帶有一個(gè)token
,在本地會(huì)對(duì)token
進(jìn)行校驗(yàn)。這種方式的優(yōu)勢(shì)在于,沒有網(wǎng)絡(luò)請(qǐng)求的限制和延時(shí)。
代理方法
在項(xiàng)目中經(jīng)常會(huì)用到代理模式傳值,代理模式在iOS
中主要分為三部分,協(xié)議、代理方、委托方三部分。
但如果使用組件化架構(gòu)的話,會(huì)涉及到組件與組件間的代理傳值,代理方需要設(shè)置為委托方的delegate
,但組件間是不可以直接產(chǎn)生耦合的。對(duì)于這種跨組件的代理情況,我們直接將代理方的對(duì)象通過MGJRouter
以參數(shù)的形式傳給另一個(gè)組件,在另一個(gè)組件中進(jìn)行代理設(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
中,代理方組件只引用這個(gè)PublicHeader.h
文件,不耦合委托方內(nèi)部代碼。為了避免定義的代理方法中出現(xiàn)耦合的情況,方法中不能出現(xiàn)和組件內(nèi)部業(yè)務(wù)有關(guān)的對(duì)象,只能傳遞系統(tǒng)的類。如果涉及到交互的情況,則通過協(xié)議方法的返回值進(jìn)行。
組件傳參
MGJRouter
可以在openURL:
時(shí)傳入一個(gè)NSDictionary
參數(shù),在接觸RAC
之后,我在想是不是可以把NSDictionary
參數(shù)變?yōu)?code>RACSignal參數(shù),直接傳一個(gè)信號(hào)過去。
注冊(cè)MGJRouter
:
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"劉小壯"];
return [RACDisposable disposableWithBlock:^{
NSLog(@"disposable");
}];
}];
[MGJRouter registerURLPattern:@"CTB://UserCenter/getUserInfo" withSignal:signal];
調(diào)用MGJRouter
:
RACSignal *signal = [MGJRouter openURL:@"CTB://UserCenter/getUserInfo"];
[signal subscribeNext:^(NSString *userName) {
NSLog(@"userName %@", userName);
}];
這種方式是可行的。使用RACSignal
方式優(yōu)點(diǎn)在于,相對(duì)于直接傳字典過去更加靈活,并且具備RAC
的諸多特性。但缺點(diǎn)也不少,信號(hào)控制不好亂用的話也很容易挖坑,是否使用還是看團(tuán)隊(duì)情況了。
常量定義
在項(xiàng)目中經(jīng)常會(huì)定義一些常量,例如通知名、常量字符串等,這些常量一般都和所屬組件有很強(qiáng)的關(guān)系,不好單獨(dú)拆出來放到其他組件。但是這些變量數(shù)量并不是很多,而且不是每個(gè)組件中都有。
所以,我們將這些變量都聲明在PublicHeader.h
文件中,其他組件只能引用PublicHeader.h
文件,不能引用組件內(nèi)部業(yè)務(wù)代碼,這樣就規(guī)避掉了組件間耦合的問題。
H5和Native通信
在項(xiàng)目中經(jīng)常會(huì)用到H5
頁面,如果能通過點(diǎn)擊H5
頁面調(diào)起原生頁面,這樣的話Native
和H5
的融合會(huì)更好。所以我們?cè)O(shè)計(jì)了一套H5
和Native
交互的方案,這套方案可以使用URLRouter
的方式調(diào)起原生頁面,實(shí)現(xiàn)方式也很簡(jiǎn)單,并且這套方案和H5
原本的跳轉(zhuǎn)邏輯并不沖突。
通過iOS
自帶UIWebView
創(chuàng)建一個(gè)H5
頁面后,H5
可以通過調(diào)用下面的JS
函數(shù)和Native
通信。調(diào)用時(shí)可以傳入新的URL
,這個(gè)URL
可以設(shè)置為URLRouter
的URL
。
window.location.href = 'CTB://UserCenter/UserLogin?userName=lxz&WeChatID=lz2046703959';
通過JS
刷新H5
頁面時(shí),會(huì)調(diào)用下面的代理方法。如果方法返回YES
,則會(huì)根據(jù)URL
協(xié)議進(jìn)行跳轉(zhuǎn)。
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
跳轉(zhuǎn)時(shí)系統(tǒng)會(huì)判斷通信協(xié)議,如果是HTTP
等標(biāo)準(zhǔn)協(xié)議,則會(huì)在當(dāng)前頁面進(jìn)行刷新。如果跳轉(zhuǎn)協(xié)議在URL Schame
中注冊(cè),則會(huì)通過系統(tǒng)openURL:
的方式調(diào)用到AppDelegate
的系統(tǒng)代理方法中,在代理方法中調(diào)用URLRouter
,則可以通過H5
頁面喚起原生頁面。
AppService
在應(yīng)用啟動(dòng)過程中,通常會(huì)做一些初始化操作。有些初始化操作是運(yùn)行程序所需要的,例如崩潰統(tǒng)計(jì)、建立服務(wù)器的長(zhǎng)連接等。或有的組件會(huì)對(duì)初始化操作有依賴關(guān)系,例如網(wǎng)絡(luò)組件依賴requestToken
等。
對(duì)于應(yīng)用啟動(dòng)時(shí)的初始化操作,應(yīng)該創(chuàng)建一個(gè)AppService
來統(tǒng)一管理啟動(dòng)操作,將初始化操作都放在里面,包含創(chuàng)建根控制器等。其中有的初始化操作需要盡快執(zhí)行,有的并不需要立即執(zhí)行,可以根據(jù)不同操作設(shè)定優(yōu)先級(jí),來管理所有初始化操作。
#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層設(shè)計(jì)
項(xiàng)目中存在很多的模型定義,那組件化后這些模型應(yīng)該定義在哪呢?
casatwy
對(duì)模型類的觀點(diǎn)是去Model
化,簡(jiǎn)單來說就是用字典代替Model
存儲(chǔ)數(shù)據(jù)。這對(duì)于組件化架構(gòu)來說,是解決組件之間數(shù)據(jù)傳遞的一個(gè)很好的方法。但是去Model
的方式,會(huì)存在大量的字段讀取代碼,使用起來遠(yuǎn)沒有模型類方便。
因?yàn)槟P皖愂顷P(guān)乎業(yè)務(wù)的,理論上必須放在業(yè)務(wù)層也就是業(yè)務(wù)組件這一層。但是要把模型對(duì)象從一個(gè)組件中當(dāng)做參數(shù)傳遞到另一個(gè)組件中,模型類放在調(diào)用方和被調(diào)方的哪個(gè)組件都不太合適,而且有可能不只兩個(gè)組件使用到這個(gè)模型對(duì)象。這樣的話在其他組件使用模型對(duì)象,必然會(huì)造成引用和耦合。
如果在用到這個(gè)模型對(duì)象的所有組件中,都分別維護(hù)一份相同的模型類,或者各自維護(hù)不同結(jié)構(gòu)的模型類,這樣之后業(yè)務(wù)發(fā)生改變模型類就會(huì)很麻煩,這是不可取的。
設(shè)計(jì)方案
如果將所有模型類單獨(dú)拉出來,定義一個(gè)模型組件呢?
這個(gè)看起來比較可行,將這個(gè)定義模型的組件下沉到基礎(chǔ)層,模型組件不包含業(yè)務(wù),只聲明模型對(duì)象的類。如果將原來各個(gè)組件的模型類定義都拉出來,單獨(dú)放在一個(gè)組件中,可以將原有各組件的Model
層變得很輕量,這樣對(duì)整個(gè)項(xiàng)目架構(gòu)來說也是有好處的。
在通過Router
進(jìn)行組件間調(diào)用時(shí),通過字典進(jìn)行傳值,這種方式比較靈活。在組件內(nèi)部使用Model
層時(shí),還是用模型組件中定義的Model
類。Model
層建議還是用Model
對(duì)象的形式比較方便,不建議整體使用去Model
化的設(shè)計(jì)。在接收到其他組件傳遞過來的字典參數(shù)時(shí),可以通過Model
類提供的初始化方法,或其他轉(zhuǎn)Model
框架將字典轉(zhuǎn)為Model
對(duì)象。
@interface CTBStoreWelfareListModel : NSObject
// 自定義初始化方法
- (instancetype)initWithDict:(NSDictionary *)dict;
@end
我公司持久化方案用的是CoreData
,所有模型的定義都在CoreData
組件中,則不需要再單獨(dú)創(chuàng)建一個(gè)模型組件。
動(dòng)態(tài)化構(gòu)想
我公司項(xiàng)目是一個(gè)常規(guī)的地圖類項(xiàng)目,首頁和百度、高德等主流地圖導(dǎo)航App
一樣,有很多添加在地圖上的控件。有的版本會(huì)添加控件上去,而有的版本會(huì)刪除控件,與之對(duì)應(yīng)的功能也會(huì)被隱藏。
所以,有次和組里小伙伴們開會(huì)的時(shí)候就在考慮,能不能在服務(wù)器下發(fā)代碼對(duì)首頁進(jìn)行布局!這樣就可以對(duì)首頁進(jìn)行動(dòng)態(tài)布局,例如有活動(dòng)的時(shí)候在指定時(shí)間顯示某個(gè)控件,這樣可以避免App Store
審核慢的問題。又或者線上某個(gè)模塊出現(xiàn)問題,可以緊急下架出問題的模塊。
對(duì)于這個(gè)問題,我們?cè)O(shè)計(jì)了一套動(dòng)態(tài)配置方案,這套方案可以對(duì)整個(gè)App
進(jìn)行配置。
配置表設(shè)計(jì)
對(duì)于動(dòng)態(tài)配置的問題,我們簡(jiǎn)單設(shè)計(jì)了一個(gè)配置表,初期打算在首頁上先進(jìn)行試水,以后可能會(huì)布置到更多的頁面上。這樣應(yīng)用程序各模塊的入口,都可以通過配置表來控制,并且通過Router
控制頁面間跳轉(zhuǎn),靈活性非常大。
在第一次安裝程序時(shí)使用內(nèi)置的配置表,之后每次都用服務(wù)器來替換本地的配置表,這樣就可以實(shí)現(xiàn)動(dòng)態(tài)配置應(yīng)用。下面是一個(gè)簡(jiǎn)單設(shè)計(jì)的配置數(shù)據(jù),JSON
中配置的是首頁的配置信息,用來模擬服務(wù)器下發(fā)的數(shù)據(jù),真正服務(wù)器下發(fā)的字段會(huì)比這個(gè)多很多。
{
"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/***"
}
]
}
對(duì)于服務(wù)器返回的數(shù)據(jù),我們會(huì)創(chuàng)建一套解析器,這個(gè)解析器用來將JSON
解析并“轉(zhuǎn)換”為標(biāo)準(zhǔn)的UIKit
控件。點(diǎn)擊后的事件都通過Router
進(jìn)行跳轉(zhuǎn),所以首頁的靈活性和Router
的使用程度成正比。
這套方案類似于React Native
的方案,從服務(wù)器下發(fā)頁面展示效果,但沒有React Native
功能那么全。相對(duì)而言是一個(gè)輕量級(jí)的配置方案,主要用于頁面配置。
資源動(dòng)態(tài)配置
除了頁面的配置之外,我們發(fā)現(xiàn)地圖類App
一般都存在ipa
過大的問題,這樣在下載時(shí)很消耗流量以及時(shí)間。所以我們就在想能不能把資源也做到動(dòng)態(tài)配置,在用戶運(yùn)行程序的時(shí)候再加載資源文件包。
我們想通過配置表的方式,將圖片資源文件都放到服務(wù)器上,圖片的URL
也隨配置表一起從服務(wù)器獲取。在使用時(shí)請(qǐng)求圖片并緩存到本地,成為真正的網(wǎng)絡(luò)APP
。在此基礎(chǔ)上設(shè)計(jì)緩存機(jī)制,定期清理本地的圖片緩存,減少用戶磁盤占用。
滴滴組件化架構(gòu)
之前看過滴滴iOS
負(fù)責(zé)人李賢輝的技術(shù)分享,分享的是滴滴iOS
客戶端的架構(gòu)發(fā)展歷程,下面簡(jiǎn)單總結(jié)一下。
發(fā)展歷程
滴滴在最開始的時(shí)候架構(gòu)較混亂。然后在2.0
時(shí)期重構(gòu)為MVC
架構(gòu),使項(xiàng)目劃分更加清晰。在3.0
時(shí)期上線了新的業(yè)務(wù)線,這時(shí)開始采用游戲開發(fā)中的狀態(tài)機(jī)機(jī)制,暫時(shí)可以滿足現(xiàn)有業(yè)務(wù)。
然而在后期不斷上線順風(fēng)車、代駕、巴士等多條業(yè)務(wù)線的情況下,現(xiàn)有架構(gòu)變得非常臃腫,代碼耦合嚴(yán)重。從而在2015年開始了代號(hào)為“The One”
的方案,這套方案就是滴滴的組件化方案。
架構(gòu)設(shè)計(jì)
滴滴的組件化方案,和蘑菇街方案類似,將項(xiàng)目拆分為各個(gè)組件,通過CocoaPods
來集成和管理各個(gè)組件。項(xiàng)目被拆分為業(yè)務(wù)部分和技術(shù)部分,業(yè)務(wù)部分包括專車、拼車、巴士等組件,使用一個(gè)pods
管理。技術(shù)部分則分為登錄分享、網(wǎng)絡(luò)、緩存這樣的一些基礎(chǔ)組件,分別使用不同的pods
管理。
組件間通信通過ONERouter
中間件進(jìn)行通信,ONERouter
類似于MGJRouter
,擔(dān)負(fù)起協(xié)調(diào)和調(diào)用各個(gè)組件的作用。組件間通信通過OpenURL
方法,來進(jìn)行對(duì)應(yīng)的調(diào)用。ONERouter
內(nèi)部保存一份Class-URL
的映射表,通過URL
找到Class
并發(fā)起調(diào)用,Class
的注冊(cè)放在+load
方法中進(jìn)行。
滴滴在業(yè)務(wù)組件內(nèi)部使用MVVM+MVCS
混合的架構(gòu),兩種架構(gòu)都是MVC
的衍生版本。其中MVCS
中的Store
負(fù)責(zé)數(shù)據(jù)相關(guān)邏輯,例如訂單狀態(tài)、地址管理等數(shù)據(jù)處理。通過MVVM
中的VM
給控制器瘦身,最后Controller
的代碼量就很少了。
滴滴首頁分析
滴滴文章中說道首頁只能有一個(gè)地圖實(shí)例,這在很多地圖導(dǎo)航相關(guān)應(yīng)用中都是這樣做的。滴滴首頁主控制器持有導(dǎo)航欄和地圖,每個(gè)業(yè)務(wù)線首頁控制器都添加在主控制器上,并且業(yè)務(wù)線控制器背景都設(shè)置為透明,將透明部分響應(yīng)事件傳遞到下面的地圖中,只響應(yīng)屬于自己的響應(yīng)事件。
由主控制器來切換各個(gè)業(yè)務(wù)線首頁,切換頁面后根據(jù)不同的業(yè)務(wù)線來更新地圖數(shù)據(jù)。
淘寶組件化架構(gòu)
本章節(jié)源自于宗心在阿里技術(shù)沙龍上的一次技術(shù)分享
架構(gòu)發(fā)展
淘寶iOS
客戶端初期是單工程的普通項(xiàng)目,但隨著業(yè)務(wù)的飛速發(fā)展,現(xiàn)有架構(gòu)并不能承載越來越多的業(yè)務(wù)需求,導(dǎo)致代碼間耦合很嚴(yán)重。后期開發(fā)團(tuán)隊(duì)對(duì)其不斷進(jìn)行重構(gòu),將項(xiàng)目重構(gòu)為組件化架構(gòu),淘寶iOS
和Android
兩個(gè)平臺(tái),除了某個(gè)平臺(tái)特有的一些特性或某些方案不便實(shí)施之外,大體架構(gòu)都是差不多的。
發(fā)展歷程
剛開始是普通的單工程項(xiàng)目,以傳統(tǒng)的
MVC
架構(gòu)進(jìn)行開發(fā)。隨著業(yè)務(wù)不斷的增加,導(dǎo)致項(xiàng)目非常臃腫、耦合嚴(yán)重。2013年淘寶開啟
all in 無線
計(jì)劃,計(jì)劃將淘寶變?yōu)橐粋€(gè)大的平臺(tái),將阿里系大多數(shù)業(yè)務(wù)都集成到這個(gè)平臺(tái)上,造成了業(yè)務(wù)的大爆發(fā)。
淘寶開始實(shí)行插件化架構(gòu),將每個(gè)業(yè)務(wù)模塊劃分為一個(gè)子工程,將組件以framework
二方庫的形式集成到主工程。但這種方式并沒有做到真正的拆分,還是在一個(gè)工程中使用git
進(jìn)行merge
,這樣還會(huì)造成合并沖突、不好回退等問題。迎來淘寶移動(dòng)端有史以來最大的重構(gòu),將其重構(gòu)為組件化架構(gòu)。將每個(gè)模塊當(dāng)做一個(gè)組件,每個(gè)組件都是一個(gè)單獨(dú)的項(xiàng)目,并且將組件打包成
framework
。主工程通過podfile
集成所有組件的framework
,實(shí)現(xiàn)業(yè)務(wù)之間真正的隔離,通過CocoaPods
實(shí)現(xiàn)組件化架構(gòu)。
架構(gòu)優(yōu)勢(shì)
淘寶是使用git
來做源碼管理的,在插件化架構(gòu)時(shí)需要盡可能避免merge
操作,否則在大團(tuán)隊(duì)中協(xié)作成本是很大的。而使用CocoaPods
進(jìn)行組件化開發(fā),則避免了這個(gè)問題。
在CocoaPods
中可以通過podfile
很好的配置各個(gè)組件,包括組件的增加和刪除,以及控制某個(gè)組件的版本。使用CocoaPods
的原因,很大程度是為了解決大型項(xiàng)目中,代碼管理工具merge
代碼導(dǎo)致的沖突。并且可以通過配置podfile
文件,輕松配置項(xiàng)目。
每個(gè)組件工程有兩個(gè)target
,一個(gè)負(fù)責(zé)編譯當(dāng)前組件和運(yùn)行調(diào)試,另一個(gè)負(fù)責(zé)打包framework
。先在組件工程做測(cè)試,測(cè)試完成后再集成到主工程中集成測(cè)試。
每個(gè)組件都是一個(gè)獨(dú)立app
,可以獨(dú)立開發(fā)、測(cè)試,使得業(yè)務(wù)組件更加獨(dú)立,所有組件可以并行開發(fā)。下層為上層提供能滿足需求的底層庫,保證上層業(yè)務(wù)層可以正常開發(fā),并將底層庫封裝成framework
集成到主工程中。
使用CocoaPods
進(jìn)行組件集成的好處在于,在集成測(cè)試自己組件時(shí),可以直接在本地主工程中,通過podfile
使用當(dāng)前組件源碼,可以直接進(jìn)行集成測(cè)試,不需要提交到服務(wù)器倉庫。
淘寶四層架構(gòu)
淘寶架構(gòu)的核心思想是一切皆組件,將工程中所有代碼都抽象為組件。
淘寶架構(gòu)主要分為四層,最上層是組件Bundle
(業(yè)務(wù)組件),依次往下是容器(核心層),中間件Bundle
(功能封裝),基礎(chǔ)庫Bundle
(底層庫)。容器層為整個(gè)架構(gòu)的核心,負(fù)責(zé)組件間的調(diào)度和消息派發(fā)。
總線設(shè)計(jì)
總線設(shè)計(jì):URL
路由+服務(wù)+消息。統(tǒng)一所有組件的通信標(biāo)準(zhǔn),各個(gè)業(yè)務(wù)間通過總線進(jìn)行通信。
URL總線
通過URL
總線對(duì)三端進(jìn)行了統(tǒng)一,一個(gè)URL
可以調(diào)起iOS
、Android
、前端三個(gè)平臺(tái),產(chǎn)品運(yùn)營(yíng)和服務(wù)器只需要下發(fā)一套URL
即可調(diào)用對(duì)應(yīng)的組件。
URL
路由可以發(fā)起請(qǐng)求也可以接受返回值,和MGJRouter
差不多。URL
路由請(qǐng)求可以被解析就直接拿來使用,如果不能被解析就跳轉(zhuǎn)H5
頁面。這樣就完成了一個(gè)對(duì)不存在組件調(diào)用的兼容,使用戶手中比較老的版本依然可以顯示新的組件。
服務(wù)提供一些公共服務(wù),由服務(wù)方組件負(fù)責(zé)實(shí)現(xiàn),通過Protocol
進(jìn)行調(diào)用。
消息總線
應(yīng)用通過消息總線進(jìn)行事件的中心分發(fā),類似于iOS
的通知機(jī)制。例如客戶端前后臺(tái)切換,則可以通過消息總線分發(fā)到接收消息的組件。因?yàn)橥ㄟ^URLRouter
只是一對(duì)一的進(jìn)行消息派發(fā)和調(diào)度,如果多次注冊(cè)同一個(gè)URL
,則會(huì)被覆蓋掉。
Bundle App
在組件化架構(gòu)的基礎(chǔ)上,淘寶提出Bundle App
的概念,可以通過已有組件,進(jìn)行簡(jiǎn)單配置后就可以組成一個(gè)新的app
出來。解決了多個(gè)應(yīng)用業(yè)務(wù)復(fù)用的問題,防止重復(fù)開發(fā)同一業(yè)務(wù)或功能。
Bundle
即App
,容器即OS
,所有Bundle App
被集成到OS
上,使每個(gè)組件的開發(fā)就像app
開發(fā)一樣簡(jiǎn)單。這樣就做到了從巨型app
回歸普通app
的輕盈,使大型項(xiàng)目的開發(fā)問題徹底得到了解決。
總結(jié)
各位可以來我博客評(píng)論區(qū)討論,可以討論文中提到的技術(shù)細(xì)節(jié),也可以討論自己公司架構(gòu)所遇到的問題,或自己獨(dú)到的見解等等。無論是不是架構(gòu)師或新入行的iOS
開發(fā),歡迎各位以一個(gè)討論技術(shù)的心態(tài)來討論。在評(píng)論區(qū)你的問題可以被其他人看到,這樣可能會(huì)給其他人帶來一些啟發(fā)。
Demo
地址:蘑菇街和casatwy
組件化方案,其Github
上都給出了Demo
,這里就貼出其Github
地址了。
蘑菇街-MGJRouter
casatwy-CTMediator
好多朋友在看完這篇文章后,都問有沒有Demo
。其實(shí)架構(gòu)是思想上的東西,重點(diǎn)還是理解架構(gòu)思想。文章中對(duì)思想的概述已經(jīng)很全面了,用多個(gè)項(xiàng)目的例子來描述組件化架構(gòu)。就算提供了Demo
,也沒法把Demo
套在其他工程上用,因?yàn)椴⒉灰欢ㄟm合所在的工程。
后來想了一下,我把組件化架構(gòu)的集成方式,簡(jiǎn)單寫了個(gè)Demo
,這樣可以解決很多人在架構(gòu)集成上的問題。我把Demo
放在我Github上了,用Coding的服務(wù)器來模擬我公司私有服務(wù)器,直接拿MGJRouter
來當(dāng)Demo
工程中的Router
。下面是Demo
地址,麻煩各位記得點(diǎn)個(gè)star??。
由于簡(jiǎn)書排版并不是很好,所以做了一個(gè)PDF
版的《組件化架構(gòu)漫談》,放在我Github
上了。PDF
上有文章目錄,方便閱讀,下面是地址。
如果你覺得不錯(cuò),請(qǐng)把PDF
幫忙轉(zhuǎn)到其他群里,或者你的朋友,讓更多的人了解組件化架構(gòu),衷心感謝!??
Github地址 : https://github.com/DeveloperErenLiu/ComponentArchitectureBook