您當前位置: 南順網絡>> 官方資訊>> 建站知識

【必讀】組件化架構漫談

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

在學習組件化架構的過程中,從很多高質量的博客中學到不少東西,例如蘑菇街李忠、casatwy、bang的博客。在學習過程中也遇到一些問題,在微博和QQ上和一些做iOS的朋友進行了交流,非常感謝這些朋友的幫助。

本篇文章主要針對于之前蘑菇街提出的組件化方案,以及casatwy提出的組件化方案進行分析,后面還會簡單提到滴滴、淘寶、微信的組件化架構,最后會簡單說一下我公司設計的組件化架構。

組件化架構的由來

隨著移動互聯網的不斷發展,很多程序代碼量和業務越來越多,現有架構已經不適合公司業務的發展速度了,很多都面臨著重構的問題。
在公司項目開發中,如果項目比較小,普通的單工程+MVC架構就可以滿足大多數需求了。但是像淘寶、蘑菇街、微信這樣的大型項目,原有的單工程架構就不足以滿足架構需求了。

就拿淘寶來說,淘寶在13年開啟的“All in 無線”戰略中,就將阿里系大多數業務都加入到手機淘寶中,使客戶端出現了業務的爆發。在這種情況下,單工程架構則已經遠遠不能滿足現有業務需求了。所以在這種情況下,淘寶在13年開啟了插件化架構的重構,后來在14年迎來了手機淘寶有史以來最大規模的重構,將其徹底重構為組件化架構。

蘑菇街的組件化架構

原因

在一個項目越來越大,開發人員越來越多的情況下,項目會遇到很多問題。

  • 業務模塊間劃分不清晰,模塊之間耦合度很大,非常難維護。

  • 所有模塊代碼都編寫在一個項目中,測試某個模塊或功能,需要編譯運行整個項目。

耦合嚴重的工程

為了解決上面的問題,可以考慮加一個中間層來協調模塊間的調用,所有的模塊間的調用都會經過中間層中轉。(注意看兩張圖的箭頭方向)

添加中間層

但是發現增加這個中間層后,耦合還是存在的。中間層對被調用模塊存在耦合,其他模塊也需要耦合中間層才能發起調用。這樣還是存在之前的相互耦合的問題,而且本質上比之前更麻煩了。

大體結構

所以應該做的是,只讓其他模塊對中間層產生耦合關系,中間層不對其他模塊發生耦合。
對于這個問題,可以采用組件化的架構,將每個模塊作為一個組件。并且建立一個主項目,這個主項目負責集成所有組件。這樣帶來的好處是很多的:

  • 業務劃分更佳清晰,新人接手更佳容易,可以按組件分配開發任務。

  • 項目可維護性更強,提高開發效率。

  • 更好排查問題,某個組件出現問題,直接對組件進行處理。

  • 開發測試過程中,可以只編譯自己那部分代碼,不需要編譯整個項目代碼。

    組件化結構

進行組件化開發后,可以把每個組件當做一個獨立的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參數。下面是注冊的示例代碼:

[MGJRouter registerURLPattern:@"mgj://detail?id=id" toHandler:^(NSDictionary *routerParameters) {
    // 下面可以在拿到參數后,為其他組件提供對應的服務
    NSString uid = routerParameters[@"id"];}];

通過openURL:方法傳入的URL參數,對詳情頁已經注冊的block方法發起調用。調用方式類似于GET請求,URL地址后面拼接參數。

[MGJRouter openURL:@"mgj://detail?id=404"];
也可以通過字典方式傳參,MGJRouter提供了帶有字典參數的方法,這樣就可以傳遞非字符串之外的其他類型參數。

[MGJRouter openURL:@"mgj://detail?" withParam:@{@"id" : @"404"}];

組件間傳值

有的時候組件間調用過程中,需要服務方在完成調用后返回相應的參數。蘑菇街提供了另外的方法,專門來完成這個操作。

[MGJRouter registerURLPattern:@"mgj://cart/ordercount" toObjectHandler:^id(NSDictionary *routerParamters){
    return @42;}];

通過下面的方式發起調用,并獲取服務方返回的返回值,要做的就是傳遞正確的URL和參數即可。

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類實現協議中定義的方法,完成對外提供服務的過程。下面是協議定義:

@protocol MGJUserProtocol <NSObject>- (NSString *)getUserName;@end

Class遵守協議并實現定義的方法,外界通過Protocol獲取的Class實例化為對象,調用服務方實現的協議方法。

ModuleManager的協議注冊方法,注冊時將Protocol反射為字符串當做存儲的key,將實現協議的Class當做值存儲。通過Protocol取Class的時候,就是通過Protocol從ModuleManager中將Class映射出來。

[ModuleManager registerClass:MGJUserImpl forProtocol:@protocol(MGJUserProtocol)];
調用時通過Protocol從ModuleManager中映射出注冊的Class,將獲取到的Class實例化,并調用Class實現的協議方法完成服務調用。

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。
http://upload-images.jianshu.io/upload_images/270478-106c9ecc613498bd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700

文件目錄

打開Demo后可以看到文件目錄非常清楚,在上圖中用藍框框出來的就是中間件部分,紅框框出來的就是業務組件部分。我對每個文件夾做了一個簡單的注釋,包含了其在架構中的職責。

在CTMediator中定義遠程調用和本地調用的兩個方法,其他業務相關的調用由Category完成。

// 遠程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采取定義常量字符串來解決這個問題,這樣管理也更方便。

#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的方式調用,這個過程就叫做發現服務。

我們發現,在這個方法中其實做了參數處理和內部調用的功能,這樣就可以保證組件內部的業務不受外部影響,對內部業務沒有侵入性。

- (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兩個平臺,除了某個平臺特有的一些特性或某些方案不便實施之外,大體架構都是差不多的。

發展歷程:

  1. 剛開始是普通的單工程項目,以傳統的MVC架構進行開發。隨著業務不斷的增加,導致項目非常臃腫、耦合嚴重。

  2. 2013年淘寶開啟"all in 無線"計劃,計劃將淘寶變為一個大的平臺,將阿里系大多數業務都集成到這個平臺上,造成了業務的大爆發。 淘寶開始實行插件化架構,將每個業務模塊劃分為一個組件,將組件以framework二方庫的形式集成到主工程。但這種方式并沒有做到真正的拆分,還是在一個工程中使用git進行merge,這樣還會造成合并沖突、不好回退等問題。

  3. 迎來淘寶移動端有史以來最大的重構,將其重構為組件化架構。將每個模塊當做一個組件,每個組件都是一個單獨的項目,并且將組件打包成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的輕盈,使大型項目的開發問題徹底得到了解決。

總結

留個小思考

到目前為止組件化架構文章就寫完了,文章確實挺長的,看到這里真是辛苦你了。下面留個小思考,把下面字符串復制到微信輸入框隨便發給一個好友,然后點擊下面鏈接大概也能猜到微信的組件化方案。

weixin://dl/profile

總結

各位可以來我博客評論區討論,可以討論文中提到的技術細節,也可以討論自己公司架構所遇到的問題,或自己獨到的見解等等。無論是不是架構師或新入行的iOS開發,歡迎各位以一個討論技術的心態來討論。在評論區你的問題可以被其他人看到,這樣可能會給其他人帶來一些啟發。

現在H5技術比較火,好多應用都用H5來完成一些頁面的開發,H5的跨平臺和實時更新等是非常大的優點,但其性能和交互也是缺點。如果以后客戶端能夠發展到可以動態部署線上代碼,不用打包上線應用市場,直接就可以做到原生應用更新,這樣就可以解決原生應用最大的痛點。這段時間公司項目比較忙,有時間我打算研究一下這個技術點。

Demo地址:蘑菇街和casatwy組件化方案,其Github上都給出了Demo,這里就貼出其Github地址了。

蘑菇街-MGJRouter

casatwy-CTMediator

好多朋友在看完這篇文章后,都問有沒有Demo。其實架構是思想上的東西,重點還是理解架構思想。文章中對思想的概述已經很全面了,用多個項目的例子來描述組件化架構。就算提供了Demo,也沒法把Demo套在其他工程上用,因為并不一定適合所在的工程。

后來想了一下,我把組件化架構的集成方式,簡單寫了個Demo,這樣可以解決很多人在架構集成上的問題。我把Demo放在我Github上了,用Coding的服務器來模擬我公司私有服務器,直接拿MGJRouter來當Demo工程中的Router。下面是Demo地址,麻煩各位記得點個start。




編輯:--ns868