組件化架構漫談

該文章屬于劉小壯原創(chuàng)沟突,轉載請注明:劉小壯


配圖

前段時間公司項目打算重構营罢,準確來說應該是按之前的產品邏輯重寫一個項目??力图。在重構項目之前涉及到架構選型的問題爽篷,我和組里小伙伴一起研究了一下組件化架構周蹭,打算將項目重構為組件化架構爵嗅。當然不是直接拿來照搬娇澎,還是要根據(jù)公司具體的業(yè)務需求設計架構。

在學習組件化架構的過程中睹晒,從很多高質量的博客中學到不少東西趟庄,例如蘑菇街李忠括细、casatwybang的博客戚啥。在學習過程中也遇到一些問題奋单,在微博和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缰泡、MVCMVCS等架構,根據(jù)自己的編程習慣做選擇棘钞。

MGJRouter方案

蘑菇街通過MGJRouter實現(xiàn)中間層缠借,由MGJRouter進行組件間的消息轉發(fā),從名字上來說更像是“路由器”宜猜。實現(xiàn)方式大致是泼返,在提供服務的組件中提前注冊block,然后在調用方組件中通過URL調用block姨拥,下面是調用方式绅喉。

架構設計
MGJRouter組件化架構

MGJRouter是一個單例對象,在其內部維護著一個“URL -> block”格式的注冊表叫乌,通過這個注冊表來保存服務方注冊的block柴罐,以及使調用方可以通過URL映射出block,并通過MGJRouter對服務方發(fā)起調用憨奸。

MGJRouter是所有組件的調度中心革屠,負責所有組件的調用、切換排宰、特殊處理等操作似芝,可以用來處理一切組件間發(fā)生的關系。除了原生頁面的解析外板甘,還可以根據(jù)URL跳轉H5頁面党瓮。

在服務方組件中都對外提供一個PublicHeader,在PublicHeader中聲明當前組件所提供的所有功能盐类,這樣其他組件想知道當前組件有什么功能寞奸,直接看PublicHeader即可。每一個block都對應著一個URL在跳,調用方可以通過URLblock發(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ù)况增,AndroidiOS都使用這一套URL,可以保持統(tǒng)一性训挡。

基礎組件

在項目中存在很多公共部分的東西澳骤,例如封裝的網(wǎng)絡請求、緩存澜薄、數(shù)據(jù)處理等功能为肮,以及項目中所用到的資源文件。蘑菇街將這些部分也當做組件肤京,劃分為基礎組件颊艳,位于業(yè)務組件下層。所有業(yè)務組件都使用同一套基礎組件,也可以保證公共部分的統(tǒng)一性棋枕。

Protocol方案

整體架構
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當做值存儲萤厅。通過ProtocolClass的時候橄抹,就是通過ProtocolModuleManager中將Class映射出來。

[ModuleManager registerClass:MGJUserImpl forProtocol:@protocol(MGJUserProtocol)];

調用時通過ProtocolModuleManager中映射出注冊的Class惕味,將獲取到的Class實例化楼誓,并調用Class實現(xiàn)的協(xié)議方法完成服務調用。

Class cls = [[ModuleManager sharedInstance] classForProtocol:@protocol(MGJUserProtocol)];
id userComponent = [[cls alloc] init];
NSString *userName = [userComponent getUserName];

項目調用流程

蘑菇街是MGJRouterProtocol混用的方式名挥,兩種實現(xiàn)的調用方式不同疟羹,但大體調用邏輯和實現(xiàn)思路類似。在MGJRouter不能滿足需求或調用不方便時禀倔,就可以通過Protocol的方式調用榄融。

  1. 在進入程序后,先使用MGJRouter對服務方組件進行注冊救湖。每個URL對應一個block的實現(xiàn)愧杯,block中的代碼就是組件對外提供的服務,調用方可以通過URL調用這個服務鞋既。

  2. 調用方通過MGJRouter調用openURL:方法力九,并將被調用代碼對應的URL傳入,MGJRouter會根據(jù)URL查找對應的block實現(xiàn)邑闺,從而調用組件的代碼進行通信跌前。

  3. 調用和注冊block時,block有一個字典用來傳遞參數(shù)陡舅。這樣的優(yōu)勢就是參數(shù)類型和數(shù)量理論上是不受限制的抵乓,但是需要很多硬編碼的key名在項目中。

內存管理

蘑菇街組件化方案有兩種蹭沛,ProtocolMGJRouter的方式臂寝,但都需要進行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提出的組件化架構

架構設計思路

casatwy是通過CTMediator類實現(xiàn)組件化的政己,在此類中對外提供明確參數(shù)類型的接口,接口內部通過performTarget方法調用服務方組件的Target掏愁、Action歇由。由于CTMediator類的調用是通過runtime主動發(fā)現(xiàn)服務的,所以服務方對此類是完全解耦的果港。

但如果CTMediator類對外提供的方法都放在此類中沦泌,將會對CTMediator造成極大的負擔和代碼量。解決方法就是對每個服務方組件創(chuàng)建一個CTMediatorCategory辛掠,并將對服務方的performTarget調用放在對應的Category中谢谦,這些Category都屬于CTMediator中間件,從而實現(xiàn)了感官上的接口分離萝衩。

casatwy組件化實現(xiàn)細節(jié)

對于服務方的組件來說回挽,每個組件都提供一個或多個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中定義的ModuleACategory,為其他組件提供了一個獲取控制器并跳轉的功能耕蝉,下面是代碼實現(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采取的是加前綴的方式屹培,從casatwyDemo中也可以看出默穴,其組件ModuleATarget命名為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方案中存在很多硬編碼的問題城看,例如targetaction以及參數(shù)名都是硬編碼在中間件中的杏慰,這種調用方式并不靈活直接测柠。

casatwy提出了去Model化的想法,我覺得這在組件化中傳參來說缘滥,是非常靈活的轰胁,這點我比較認同。相對于MGJRouter的話朝扼,也采用了去Model化的傳參方式赃阀,而不是直接傳遞模型對象。組件化傳參并不適用傳模型對象擎颖,但組件內部還是可以使用Model的榛斯。

MGJRouter

MGJRouter方案是一套非常輕量級的方案观游,其中間件代碼總共也就兩百行以內,非常簡潔驮俗。在調用時直接通過URL調用懂缕,調用起來很簡單,我推薦使用這套方案作為組件化架構的中間件王凑。

MGJRouter最強大的一點在于提佣,統(tǒng)一了遠程調用和本地調用。這就使得可以通過Push的方式荤崇,進行任何允許的組件間調用拌屏,對項目運營是有很大幫助的。

這三套方案都實現(xiàn)了組件間的解耦术荤,MGJRouterProtocol都是調用方對中間件的耦合倚喂,CTMediator是中間件對組件的耦合,都是單向耦合瓣戚。

接口類

在三套方案中端圈,服務方組件都對外提供一個PublicHeaderTarget,在文件中統(tǒng)一定義對外提供的服務子库,組件間通信的實現(xiàn)代碼大多數(shù)都在里面舱权。

但三套實現(xiàn)方案實現(xiàn)方式并不同,蘑菇街的兩套方案都需要注冊操作仑嗅,無論是Block還是Protocol都需要注冊后才可以提供服務宴倍。而casatwy的方案則不需要,直接通過runtime調用仓技。

組件化架構設計

在上面文章中提到了casatwy方案的CTMediator鸵贬,蘑菇街方案的MGJRouterModuleManager,之后將統(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 updateCocoaPods操作挟炬。

這兩種組件集成方案,實踐中也是各有利弊。直接在主工程中集成代碼文件谤祖,可以看到其內部實現(xiàn)源碼婿滓,方便在主工程中進行調試。集成framework的方式粥喜,可以加快編譯速度凸主,而且對每個組件的代碼有很好的保密性。如果公司對代碼安全比較看重额湘,可以考慮framework的形式卿吐。

例如手機QQ或者支付寶這樣的大型程序,一般都會采取framework的形式锋华。而且一般這樣的大公司嗡官,都會有自己的組件庫,這個組件庫往往可以代表一個大的功能或業(yè)務組件毯焕,直接添加項目中就可以使用衍腥。關于組件化庫在后面講淘寶組件化架構的時候會提到。

資源文件

對于項目中圖片的集成芥丧,可以把圖片當做一個單獨的組件紧阔,組件中只存在圖片文件,沒有任何代碼续担。圖片可以使用Bundleimage assets進行管理擅耽,如果是Bundle就針對不同業(yè)務模塊建立不同的Bundle,如果是image assets物遇,就按照不同的模塊分類建立不同的assets乖仇,將所有資源放在同一個組件內。

Bundleimage assets兩者相比询兴,我還是更推薦用assets的方式乃沙,因為assets自身提供很多功能(例如設置圖片拉伸范圍),而且在打包之后圖片會被打包在.cer文件中诗舰,不會被看到警儒。(現(xiàn)在也可以通過工具對.cer文件進行解析,獲取里面的圖片)

使用Cocoapods眶根,所有的資源文件都放置在一個podspec中蜀铲,主工程可以直接引用這個podspec,假設此podspec名為:Assets属百,而這個Assetspodspec里面配置信息可以寫為:

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é)議名的方式,使用配置表控制H5native的切換醋虏,配置表可以從后臺更新寻咒,只需要將協(xié)議名更改一下即可。

mgj://detail?id=123456
http://www.mogujie.com/detail?id=123456

假設現(xiàn)在線上的native組件出現(xiàn)嚴重bug颈嚼,在后臺將配置文件中原有的本地URL換成H5URL毛秘,并更新客戶端配置文件。

在調用MGJRouter時傳入這個H5URL即可完成切換阻课,MGJRouter判斷如果傳進來的是一個H5URL就直接跳轉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)吐句,可以是任意方式的架構胁后,MVVMMVC嗦枢、MVCS等都是可以的攀芯,只要通過MGJRouter將組件間的通信方式統(tǒng)一即可。

分層架構

組件化架構在物理結構上來說是不分層次的文虏,只有組件與組件之間的劃分關系侣诺。但是在組件化架構的基礎上,應該根據(jù)項目和業(yè)務設計自己的層次架構氧秘,這套層次架構可以用來區(qū)分組件所處的層次及職責年鸳,所以我們設計了層級架構+組件化架構的整體架構。

我公司項目最開始設計的是三層架構:業(yè)務層 -> 核心層 (high + low) -> 基礎層丸相,其中核心層又分為highlow兩部分搔确。但是這種架構會造成核心層過重,基礎層過輕的問題灭忠,這種并不適合組件化架構膳算。

在三層架構中會發(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ù)叔壤。

組件Router表
路由層安全

組件化架構需要注意路由層的安全問題。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頁面調起原生頁面束莫,這樣的話NativeH5的融合會更好懒棉。所以我們設計了一套H5Native交互的方案,這套方案可以使用URLRouter的方式調起原生頁面览绿,實現(xiàn)方式也很簡單策严,并且這套方案和H5原本的跳轉邏輯并不沖突。

通過iOS自帶UIWebView創(chuàng)建一個H5頁面后饿敲,H5可以通過調用下面的JS函數(shù)和Native通信妻导。調用時可以傳入新的URL,這個URL可以設置為URLRouterURL诀蓉。

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ā)團隊對其不斷進行重構,將項目重構為組件化架構嘴脾,淘寶iOSAndroid兩個平臺男摧,除了某個平臺特有的一些特性或某些方案不便實施之外,大體架構都是差不多的译打。

發(fā)展歷程

  1. 剛開始是普通的單工程項目耗拓,以傳統(tǒng)的MVC架構進行開發(fā)。隨著業(yè)務不斷的增加奏司,導致項目非常臃腫乔询、耦合嚴重。

  2. 2013年淘寶開啟all in 無線計劃韵洋,計劃將淘寶變?yōu)橐粋€大的平臺竿刁,將阿里系大多數(shù)業(yè)務都集成到這個平臺上,造成了業(yè)務的大爆發(fā)麻献。
    淘寶開始實行插件化架構们妥,將每個業(yè)務模塊劃分為一個子工程,將組件以framework二方庫的形式集成到主工程勉吻。但這種方式并沒有做到真正的拆分监婶,還是在一個工程中使用git進行merge,這樣還會造成合并沖突齿桃、不好回退等問題惑惶。

  3. 迎來淘寶移動端有史以來最大的重構,將其重構為組件化架構短纵。將每個模塊當做一個組件带污,每個組件都是一個單獨的項目歪泳,并且將組件打包成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(圖片來自淘寶技術分享)

在組件化架構的基礎上,淘寶提出Bundle App的概念觉既,可以通過已有組件惧盹,進行簡單配置后就可以組成一個新的app出來。解決了多個應用業(yè)務復用的問題奋救,防止重復開發(fā)同一業(yè)務或功能岭参。

BundleApp,容器即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??汤功。

組件化架構集成Demo

由于簡書排版并不是很好物邑,所以做了一個PDF版的《組件化架構漫談》,放在我Github上了滔金。PDF上有文章目錄色解,方便閱讀,下面是地址餐茵。

如果你覺得不錯科阎,請把PDF幫忙轉到其他群里,或者你的朋友忿族,讓更多的人了解組件化架構锣笨,衷心感謝!??

組件化架構PDF

Github地址 : https://github.com/DeveloperErenLiu/ComponentArchitectureBook

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末道批,一起剝皮案震驚了整個濱河市错英,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌隆豹,老刑警劉巖椭岩,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡判哥,警方通過查閱死者的電腦和手機献雅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來姨伟,“玉大人惩琉,你說我怎么就攤上這事《峄模” “怎么了瞒渠?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長技扼。 經(jīng)常有香客問我伍玖,道長,這世上最難降的妖魔是什么剿吻? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任窍箍,我火速辦了婚禮,結果婚禮上丽旅,老公的妹妹穿的比我還像新娘椰棘。我一直安慰自己,他們只是感情好榄笙,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布邪狞。 她就那樣靜靜地躺著,像睡著了一般茅撞。 火紅的嫁衣襯著肌膚如雪帆卓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天米丘,我揣著相機與錄音剑令,去河邊找鬼。 笑死拄查,一個胖子當著我的面吹牛吁津,可吹牛的內容都是我干的。 我是一名探鬼主播靶累,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼腺毫,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了挣柬?” 一聲冷哼從身側響起潮酒,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎邪蛔,沒想到半個月后急黎,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年勃教,在試婚紗的時候發(fā)現(xiàn)自己被綠了淤击。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡故源,死狀恐怖污抬,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情绳军,我是刑警寧澤印机,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站门驾,受9級特大地震影響射赛,放射性物質發(fā)生泄漏。R本人自食惡果不足惜奶是,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一楣责、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧聂沙,春花似錦秆麸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至豁生,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間漫贞,已是汗流浹背甸箱。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留迅脐,地道東北人芍殖。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像谴蔑,于是被迫代替她去往敵國和親豌骏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內容