經(jīng)驗分享 | 超詳細(xì)的組件化架構(gòu)方案


文/劉小壯(簡書作者投稿)

原文鏈接:http://www.itdecent.cn/p/67a6004f6930

前段時間公司項目打算重構(gòu),準(zhǔn)確來說應(yīng)該是按之前的產(chǎn)品邏輯重寫一個項目??。在重構(gòu)項目之前涉及到架構(gòu)選型的問題,我和組里小伙伴一起研究了一下組件化架構(gòu),打算將項目重構(gòu)為組件化架構(gòu)。當(dāng)然不是直接拿來照搬,還是要根據(jù)公司具體的業(yè)務(wù)需求設(shè)計架構(gòu)。

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

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


組件化架構(gòu)的由來

隨著移動互聯(lián)網(wǎng)的不斷發(fā)展,很多程序代碼量和業(yè)務(wù)越來越多,現(xiàn)有架構(gòu)已經(jīng)不適合公司業(yè)務(wù)的發(fā)展速度了,很多都面臨著重構(gòu)的問題。

在公司項目開發(fā)中,如果項目比較小,普通的單工程+MVC架構(gòu)就可以滿足大多數(shù)需求了。但是像淘寶、蘑菇街、微信這樣的大型項目,原有的單工程架構(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),將其徹底重構(gòu)為組件化架構(gòu)。

蘑菇街的組件化架構(gòu)

原因

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

  • 業(yè)務(wù)模塊間劃分不清晰,模塊之間耦合度很大,非常難維護(hù)。

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


  • 耦合嚴(yán)重的工程

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

    添加中間層

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

    大體結(jié)構(gòu)

    所以應(yīng)該做的是,只讓其他模塊對中間層產(chǎn)生耦合關(guān)系,中間層不對其他模塊發(fā)生耦合。

    對于這個問題,可以采用組件化的架構(gòu),將每個模塊作為一個組件。并且建立一個主項目,這個主項目負(fù)責(zé)集成所有組件。這樣帶來的好處是很多的:

  • 業(yè)務(wù)劃分更佳清晰,新人接手更佳容易,可以按組件分配開發(fā)任務(wù)。

  • 項目可維護(hù)性更強(qiáng),提高開發(fā)效率。

  • 更好排查問題,某個組件出現(xiàn)問題,直接對組件進(jìn)行處理。

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


  • 組件化結(jié)構(gòu)

    進(jìn)行組件化開發(fā)后,可以把每個組件當(dāng)做一個獨立的app,每個組件甚至可以采取不同的架構(gòu),例如分別使用MVVM、MVC、MVCS等架構(gòu)。

    MGJRouter方案

    蘑菇街通過MGJRouter實現(xiàn)中間層,通過MGJRouter進(jìn)行組件間的消息轉(zhuǎn)發(fā),從名字上來說更像是路由器。實現(xiàn)方式大致是,在提供服務(wù)的組件中提前注冊block,然后在調(diào)用方組件中通過URL調(diào)用block,下面是調(diào)用方式。

    架構(gòu)設(shè)計

    MGJRouter組件化架構(gòu)

    MGJRouter是一個單例對象,在其內(nèi)部維護(hù)著一個“URL -> block”格式的注冊表,通過這個注冊表來保存服務(wù)方注冊的block,以及使調(diào)用方可以通過URL映射出block,并通過MGJRouter對服務(wù)方發(fā)起調(diào)用。

    在服務(wù)方組件中都對外提供一個接口類,在接口類內(nèi)部實現(xiàn)block的注冊工作,以及block對外提供服務(wù)的代碼實現(xiàn)。每一個block都對應(yīng)著一個URL,調(diào)用方可以通過URL對block發(fā)起調(diào)用。

    在程序開始運行時,需要將所有服務(wù)方的接口類實例化,以完成這個注冊工作,使MGJRouter中所有服務(wù)方的block可以正常提供服務(wù)。在這個服務(wù)注冊完成后,就可以被調(diào)用方調(diào)起并提供服務(wù)。

    蘑菇街項目使用git作為版本控制工具,將每個組件都當(dāng)做一個獨立工程,并建立主項目來集成所有組件。集成方式是在主項目中通過CocoaPods來集成,將所有組件當(dāng)做二方庫集成到項目中。詳細(xì)的集成技術(shù)點在下面“標(biāo)準(zhǔn)組件化架構(gòu)設(shè)計”章節(jié)中會講到。

    MGJRouter調(diào)用

    代碼模擬對詳情頁的注冊、調(diào)用,在調(diào)用過程中傳遞id參數(shù)。下面是注冊的示例代碼:

    [MGJRouter?registerURLPattern:@"mgj://detail?id=id"?toHandler:^(NSDictionary *routerParameters)?{

    // 下面可以在拿到參數(shù)后,為其他組件提供對應(yīng)的服務(wù)

    ?????NSString?uid?=?routerParameters[@"id"];

    }];

    通過openURL:方法傳入的URL參數(shù),對詳情頁已經(jīng)注冊的block方法發(fā)起調(diào)用。調(diào)用方式類似于GET請求,URL地址后面拼接參數(shù)。

    [MGJRouter openURL:@"mgj://detail?id=404"];

    也可以通過字典方式傳參,MGJRouter提供了帶有字典參數(shù)的方法,這樣就可以傳遞非字符串之外的其他類型參數(shù)。

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

    組件間傳值

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

    [MGJRouter?registerURLPattern:@"mgj://cart/ordercount"?toObjectHandler:^id(NSDictionary *routerParamters){

    ?????return?@42;

    }];

    通過下面的方式發(fā)起調(diào)用,并獲取服務(wù)方返回的返回值,要做的就是傳遞正確的URL和參數(shù)即可。

    NSNumber *orderCount?=?[MGJRouter?objectForURL:@"mgj://cart/ordercount"];

    短鏈管理

    這時候會發(fā)現(xiàn)一個問題,在蘑菇街組件化架構(gòu)中,存在了很多硬編碼的URL和參數(shù)。在代碼實現(xiàn)過程中URL編寫出錯會導(dǎo)致調(diào)用失敗,而且參數(shù)是一個字典類型,調(diào)用方不知道服務(wù)方需要哪些參數(shù),這些都是個問題。

    對于這些數(shù)據(jù)的管理,蘑菇街開發(fā)了一個web頁面,這個web頁面統(tǒng)一來管理所有的URL和參數(shù),Android和iOS都使用這一套URL,可以保持統(tǒng)一性。

    基礎(chǔ)組件

    在項目中存在很多公共部分的東西,例如封裝的網(wǎng)絡(luò)請求、緩存、數(shù)據(jù)處理等功能,以及項目中所用到的資源文件。

    蘑菇街將這些部分也當(dāng)做組件,劃分為基礎(chǔ)組件,位于業(yè)務(wù)組件下層。所有業(yè)務(wù)組件都使用同一個基礎(chǔ)組件,也可以保證公共部分的統(tǒng)一性。

    Protocol方案

    整體架構(gòu)

    Protocol方案的中間件

    為了解決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é)議比較多也可以分開幾個文件定義。這樣所有調(diào)用方依然是只依賴中間件,不需要依賴除中間件之外的其他組件。

    Protocol方案中每個組件也需要一個“接口類”,此類負(fù)責(zé)實現(xiàn)當(dāng)前組件對應(yīng)的協(xié)議方法,也就是對外提供服務(wù)的實現(xiàn)。在程序開始運行時將自身的Class注冊到ModuleManager中,并將Protocol反射出字符串當(dāng)做key。這個注冊過程和MGJRouter是類似的,都需要提前注冊服務(wù)。

    示例代碼

    創(chuàng)建MGJUserImpl類當(dāng)做User模塊的服務(wù)類,并在MGJComponentProtocol.h中定義MGJUserProtocol協(xié)議,由MGJUserImpl類實現(xiàn)協(xié)議中定義的方法,完成對外提供服務(wù)的過程。下面是協(xié)議定義:

    @protocol?MGJUserProtocol

    -?(NSString *)getUserName;

    @end

    Class遵守協(xié)議并實現(xiàn)定義的方法,外界通過Protocol獲取的Class實例化為對象,調(diào)用服務(wù)方實現(xiàn)的協(xié)議方法。

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

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

    調(diào)用時通過Protocol從ModuleManager中映射出注冊的Class,將獲取到的Class實例化,并調(diào)用Class實現(xiàn)的協(xié)議方法完成服務(wù)調(diào)用。

    Class?cls?=?[[ModuleManager?sharedInstance]?classForProtocol:@protocol(MGJUserProtocol)];

    id?userComponent?=?[[cls?alloc]?init];

    NSString *userName?=?[userComponent?getUserName];

    整體調(diào)用流程

    蘑菇街是OpenURL和Protocol混用的方式,兩種實現(xiàn)的調(diào)用方式不同,但大體調(diào)用邏輯和實現(xiàn)思路類似,所以下面的調(diào)用流程二者差不多。在OpenURL不能滿足需求或調(diào)用不方便時,就可以通過Protocol的方式調(diào)用。

  • 在進(jìn)入程序后,先使用MGJRouter對服務(wù)方組件進(jìn)行注冊。每個URL對應(yīng)一個block的實現(xiàn),block中的代碼就是服務(wù)方對外提供的服務(wù),調(diào)用方可以通過URL調(diào)用這個服務(wù)。

  • 調(diào)用方通過MGJRouter調(diào)用openURL:方法,并將被調(diào)用代碼對應(yīng)的URL傳入,MGJRouter會根據(jù)URL查找對應(yīng)的block實現(xiàn),從而調(diào)用服務(wù)方組件的代碼進(jìn)行通信。

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

  • 內(nèi)存管理

    蘑菇街組件化方案有兩種,Protocol和MGJRouter的方式,但都需要進(jìn)行register操作。Protocol注冊的是Class,MGJRouter注冊的是Block,注冊表是一個NSMutableDictionary類型的字典,而字典的擁有者又是一個單例對象,這樣會造成內(nèi)存的常駐。

    下面是對兩種實現(xiàn)方式內(nèi)存消耗的分析:

  • 首先說一下block實現(xiàn)方式可能導(dǎo)致的內(nèi)存問題,block如果使用不當(dāng),很容易造成循環(huán)引用的問題。

    經(jīng)過暴力測試,證明并不會導(dǎo)致內(nèi)存問題。被保存在字典中是一個block對象,而block對象本身并不會占用多少內(nèi)存。在調(diào)用block后會對block體中的方法進(jìn)行執(zhí)行,執(zhí)行完成后block體中的對象釋放。

    而block自身的實現(xiàn)只是一個結(jié)構(gòu)體,也就相當(dāng)于字典中存放的是很多結(jié)構(gòu)體,所以內(nèi)存的占用并不是很大。

  • 對于協(xié)議這種實現(xiàn)方式,和block內(nèi)存常駐方式差不多。只是將存儲的block對象換成Class對象,如果不是已經(jīng)實例化的對象,內(nèi)存占用還是比較小的。

  • casatwy組件化方案

    整體架構(gòu)

    casatwy組件化方案分為兩種調(diào)用方式,遠(yuǎn)程調(diào)用和本地調(diào)用,對于兩個不同的調(diào)用方式分別對應(yīng)兩個接口。


  • 遠(yuǎn)程調(diào)用通過AppDelegate代理方法傳遞到當(dāng)前應(yīng)用后,調(diào)用遠(yuǎn)程接口并在內(nèi)部做一些處理,處理完成后會在遠(yuǎn)程接口內(nèi)部調(diào)用本地接口,以實現(xiàn)本地調(diào)用為遠(yuǎn)程調(diào)用服務(wù)。

  • 本地調(diào)用由performTarget:action:params:方法負(fù)責(zé),但調(diào)用方一般不直接調(diào)用performTarget:方法。CTMediator會對外提供明確參數(shù)和方法名的方法,在方法內(nèi)部調(diào)用performTarget:方法和參數(shù)的轉(zhuǎn)換。



  • casatwy提出的組件化架構(gòu)


    架構(gòu)設(shè)計思路



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


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



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


    對于服務(wù)方的組件來說,每個組件都提供一個或多個Target類,在Target類中聲明Action方法。Target類是當(dāng)前組件對外提供的一個“服務(wù)類”,Target將當(dāng)前組件中所有的服務(wù)都定義在里面,CTMediator通過runtime主動發(fā)現(xiàn)服務(wù)。


    在Target中的所有Action方法,都只有一個字典參數(shù),所以可以傳遞的參數(shù)很靈活,這也是casatwy提出的去Model化的概念。在Action的方法實現(xiàn)中,對傳進(jìn)來的字典參數(shù)進(jìn)行解析,再調(diào)用組件內(nèi)部的類和方法。


    架構(gòu)分析



    casatwy為我們提供了一個Demo,通過這個Demo可以很好的理解casatwy的設(shè)計思路,下面按照我的理解講解一下這個Demo。

    https://github.com/casatwy/CTMediator



    文件目錄


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


    在CTMediator中定義遠(yuǎn)程調(diào)用和本地調(diào)用的兩個方法,其他業(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,對外提供了一個獲取控制器并跳轉(zhuǎn)的功能,下面是代碼實現(xiàn)。由于casatwy的方案中使用performTarget的方式進(jìn)行調(diào)用,所以涉及到很多硬編碼字符串的問題,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?{

    // 這里處理異常場景,具體如何處理取決于產(chǎn)品

    ????????return?[[UIViewController?alloc]?init];

    }

    }


    下面是ModuleA組件中提供的服務(wù),被定義在Target_A類中,這些服務(wù)可以被CTMediator通過runtime的方式調(diào)用,這個過程就叫做發(fā)現(xiàn)服務(wù)。


    我們發(fā)現(xiàn),在這個方法中其實做了參數(shù)處理和內(nèi)部調(diào)用的功能,這樣就可以保證組件內(nèi)部的業(yè)務(wù)不受外部影響,對內(nèi)部業(yè)務(wù)沒有侵入性。


    -?(UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params?{

    // 對傳過來的字典參數(shù)進(jìn)行解析,并調(diào)用ModuleA內(nèi)部的代碼

    ????DemoModuleADetailViewController *viewController?=?[[DemoModuleADetailViewControlleralloc]?init];

    ????viewController.valueLabel.text?=?params[@"key"];

    ????return?viewController;

    }


    命名規(guī)范



    在大型項目中代碼量比較大,需要避免命名沖突的問題。對于這個問題casatwy采取的是加前綴的方式,從casatwy的Demo中也可以看出,其組件ModuleA的Target命名為Target_A,被調(diào)用的Action命名為Action_nativeFetchDetailViewController:。


    casatwy將類和方法的命名,都統(tǒng)一按照其功能做區(qū)分當(dāng)做前綴,這樣很好的將組件相關(guān)和組件內(nèi)部代碼進(jìn)行了劃分。


    標(biāo)準(zhǔn)組件化架構(gòu)設(shè)計



    這個章節(jié)叫做“標(biāo)準(zhǔn)組件化架構(gòu)設(shè)計”,對于項目架構(gòu)來說并沒有絕對意義的標(biāo)準(zhǔn)之說。這里說到的“標(biāo)準(zhǔn)組件化架構(gòu)設(shè)計”只是因為采取這樣的方式的人比較多,且這種方式相比而言較合理。


    在上面文章中提到了casatwy方案的CTMediator,蘑菇街方案的MGJRouter和ModuleManager,下面統(tǒng)稱為中間件。


    整體架構(gòu)



    組件化架構(gòu)中,首先有一個主工程,主工程負(fù)責(zé)集成所有組件。每個組件都是一個單獨的工程,創(chuàng)建不同的git私有倉庫來管理,每個組件都有對應(yīng)的開發(fā)人員負(fù)責(zé)開發(fā)。開發(fā)人員只需要關(guān)注與其相關(guān)組件的代碼,其他業(yè)務(wù)代碼和其無關(guān),來新人也好上手。


    組件的劃分需要注意組件粒度,粒度根據(jù)業(yè)務(wù)可大可小。組件劃分后屬于業(yè)務(wù)組件,對于一些多個組件共同的東西,例如網(wǎng)絡(luò)、數(shù)據(jù)庫之類的,應(yīng)該劃分到單獨的組件或基礎(chǔ)組件中。對于圖片或配置表這樣的資源文件,應(yīng)該再單獨劃分一個資源組件,這樣避免資源的重復(fù)性。


    服務(wù)方組件對外提供服務(wù),由中間件調(diào)用或發(fā)現(xiàn)服務(wù),服務(wù)對當(dāng)前組件無侵入性,只負(fù)責(zé)對傳遞過來的數(shù)據(jù)進(jìn)行解析和組件內(nèi)調(diào)用的功能。需要被其他組件調(diào)用的組件都是服務(wù)方,服務(wù)方也可以調(diào)用其他組件的服務(wù)。


    通過這樣的組件劃分,組件的開發(fā)進(jìn)度不會受其他業(yè)務(wù)的影響,可以多個組件單獨的并行開發(fā)。組件間的通信都交給中間件來進(jìn)行,需要通信的類只需要接觸中間件,而中間件不需要耦合其他組件,這就實現(xiàn)了組件間的解耦。中間件負(fù)責(zé)處理所有組件之間的調(diào)度,在所有組件之間起到控制核心的作用。


    這套框架清晰的劃分了不同組件,從整體架構(gòu)上來約束開發(fā)人員進(jìn)行組件化開發(fā),避免某個開發(fā)人員偷懶直接引用頭文件,產(chǎn)生組件間的耦合,破壞整體架構(gòu)。假設(shè)以后某個業(yè)務(wù)發(fā)生大的改變,需要對相關(guān)代碼進(jìn)行重構(gòu),可以在單個組件進(jìn)行重構(gòu)。組件化架構(gòu)降低了重構(gòu)的風(fēng)險,保證了代碼的健壯性。


    組件集成




    組件化架構(gòu)圖


    每個組件都是一個單獨的工程,在組件開發(fā)完成后上傳到git倉庫。主工程通過Cocoapods集成各個組件,集成和更新組件時只需要pod update即可。這樣就是把每個組件當(dāng)做第三方來管理,管理起來非常方便。


    Cocoapods可以控制每個組件的版本,例如在主項目中回滾某個組件到特定版本,就可以通過修改podfile文件實現(xiàn)。選擇Cocoapods主要因為其本身功能很強(qiáng)大,可以很方便的集成整個項目,也有利于代碼的復(fù)用。通過這種集成方式,可以很好的避免在傳統(tǒng)項目中代碼沖突的問題。


    集成方式



    對于組件化架構(gòu)的集成方式,我在看完bang的博客后專門請教了一下bang。根據(jù)在微博上和bang的聊天以及其他博客中的學(xué)習(xí),在主項目中集成組件主要分為兩種方式——源碼和framework,但都是通過CocoaPods來集成。

    無論是用CocoaPods管理源碼,還是直接管理framework,效果都是一樣的,都是可以直接進(jìn)行pod update之類的操作的。


    這兩種組件集成方案,實踐中也是各有利弊。直接在主工程中集成代碼文件,可以在主工程中進(jìn)行調(diào)試。集成framework的方式,可以加快編譯速度,而且對每個組件的代碼有很好的保密性。如果公司對代碼安全比較看重,可以考慮framework的形式,但framework不利于主工程中的調(diào)試。


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


    不推薦的集成方式



    之前有些項目是直接用workspace的方式集成的,或者直接在原有項目中建立子項目,直接做文件引用。但這兩點都是不建議做的,因為沒有真正意義上實現(xiàn)業(yè)務(wù)組件的剝離,只是像之前的項目一樣從文件目錄結(jié)構(gòu)上進(jìn)行了劃分。




    組件化開發(fā)總結(jié)



    對于項目架構(gòu)來說,一定要建立于業(yè)務(wù)之上來設(shè)計架構(gòu)。不同的項目業(yè)務(wù)不同,組件化方案的設(shè)計也會不同,應(yīng)該設(shè)計最適合公司業(yè)務(wù)的架構(gòu)。


    架構(gòu)對比



    在除蘑菇街Protocol方案外,其他兩種方案都或多或少的存在硬編碼問題,硬編碼如果量比較大的話挺麻煩的。

    在casatwy的CTMediator方案中需要硬編碼Target、Action字符串,只不過這個缺陷被封閉在中間件里面了,將這些字符串都統(tǒng)一定義為常量,外界使用不需要接觸到硬編碼。蘑菇街的MGJRouter的方案也是一樣的,也有硬編碼URL的問題,蘑菇街可能也做了類似的處理。


    casatwy和蘑菇街提出的兩套組件化方案,大體結(jié)構(gòu)是類似的,三套方案都分為調(diào)用方、中間件、服務(wù)方,只是在具體實現(xiàn)過程中有些不同。例如Protocol方案在中間件中加入了Protocol文件,casatwy的方案在中間件中加入了Category。

    三種方案內(nèi)部都有容錯處理,所以三種方案的穩(wěn)定性都是比較好的,而且都可以拿出來單獨運行,在服務(wù)方不存在的情況下也不會有問題。


    在三套方案中,服務(wù)方都對外提供一個供外界調(diào)用的接口類,這個類中實現(xiàn)組件對外提供的服務(wù),中間件通過接口類來實現(xiàn)組件間的通信。在此類中統(tǒng)一定義對外提供的服務(wù),外界調(diào)用時就知道服務(wù)方可以做什么。


    調(diào)用流程也不大一樣,蘑菇街的兩套方案都需要注冊操作,無論是Block還是Protocol都需要注冊后才可以提供服務(wù)。而casatwy的方案則不需要,直接通過runtime調(diào)用。casatwy的方案實現(xiàn)了真正的對服務(wù)方解耦,而蘑菇街的兩套方案則沒有,對服務(wù)方和調(diào)用方都造成了耦合。


    我認(rèn)為三套方案中,Protocol方案是調(diào)用和維護(hù)最麻煩的一套方案。維護(hù)時需要同時維護(hù)Protocol、接口類兩部分。而且調(diào)用時需要將服務(wù)方的接口類返回給調(diào)用方,并由調(diào)用方執(zhí)行一系列調(diào)用邏輯,調(diào)用一個服務(wù)的邏輯非常復(fù)雜,這在開發(fā)中是非常影響開發(fā)效率的。


    總結(jié)



    下面是組件化開發(fā)中的一個小總結(jié),也是開發(fā)過程中的一些注意點。


  • 在MGJRouter方案中,是通過調(diào)用OpenURL:方法并傳入URL來發(fā)起調(diào)用。鑒于URL協(xié)議名等固定格式,可以通過判斷協(xié)議名的方式,使用配置表控制H5和native的切換,配置表可以從后臺更新,只需要將協(xié)議名更改一下即可。


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


    假設(shè)現(xiàn)在線上的native組件出現(xiàn)嚴(yán)重bug,在后臺將配置文件中原有的本地URL換成H5的URL,并更新客戶端配置文件。在調(diào)用MGJRouter時傳入這個H5的URL即可完成切換,MGJRouter判斷如果傳進(jìn)來的是一個H5的URL就直接跳轉(zhuǎn)webView。而且URL可以傳遞參數(shù)給MGJRouter,只需要MGJRouter內(nèi)部做參數(shù)截取即可。


  • casatwy方案和蘑菇街Protocol方案,都提供了傳遞明確類型參數(shù)的方法。在MGJRouter方案中,傳遞參數(shù)主要是通過類似GET請求一樣在URL后面拼接參數(shù),和在字典中傳遞參數(shù)兩種方式組成。這兩種方式會造成傳遞參數(shù)類型不明確,傳遞參數(shù)類型受限(GET請求不能傳遞對象)等問題,后來使用Protocol方案彌補(bǔ)這個問題。

  • 組件化開發(fā)可以很好的提升代碼復(fù)用性,組件可以直接拿到其他項目中使用,這個優(yōu)點在下面淘寶架構(gòu)中會著重講一下。

  • 對于調(diào)試工作,應(yīng)該放在每個組件中完成。單獨的業(yè)務(wù)組件可以直接提交給測試提測,這樣測試起來也比較方便。最后組件開發(fā)完成并測試通過后,再將所有組件更新到主項目,提交給測試進(jìn)行集成測試即可。

  • 使用組件化架構(gòu)開發(fā),組件間的通信都是有成本的。所以盡量將業(yè)務(wù)封裝在組件內(nèi)部,對外只提供簡單的接口。即“高內(nèi)聚、低耦合”原則。

  • 把握好劃分粒度的細(xì)化程度,太細(xì)則項目過于分散,太大則項目組件臃腫。但是項目都是從小到大的一個發(fā)展過程,所以不斷進(jìn)行重構(gòu)是掌握這個組件的細(xì)化程度最好的方式。


  • 我公司架構(gòu)



    下面就簡單說說我公司項目架構(gòu),公司項目是一個地圖導(dǎo)航應(yīng)用,業(yè)務(wù)層之下的基礎(chǔ)組件占比較大。且基礎(chǔ)組件相對比較獨立,對外提供了很多調(diào)用接口。剛開始想的是采用MGJRouter的方案,但如果這些調(diào)用都通過Router進(jìn)行,開發(fā)起來比較復(fù)雜,反而會適得其反。最主要我們項目也并不是非常大,沒必要都用Router轉(zhuǎn)發(fā)。


    對于這個問題,公司項目的架構(gòu)設(shè)計是:層級架構(gòu)+組件化架構(gòu),組件化架構(gòu)處于層級架構(gòu)的最上層,也就是業(yè)務(wù)層。采取這種結(jié)構(gòu)混合的方式進(jìn)行整體架構(gòu),這個對于公共組件的管理和層級劃分比較有利,符合公司業(yè)務(wù)需求。



    公司組件化架構(gòu)


    對于業(yè)務(wù)層級依然采用組件化架構(gòu)的設(shè)計,這樣可以充分利用組件化架構(gòu)的優(yōu)勢,對項目組件間進(jìn)行解耦。在上層和下層的調(diào)用中,下層的功能組件應(yīng)該對外開放一個接口類,在接口類中聲明所有的服務(wù),實現(xiàn)上層調(diào)用當(dāng)前組件的一個中轉(zhuǎn),上層直接調(diào)用接口類。這樣做的好處在于,如果下層發(fā)生改變不會對上層造成影響,而且也省去了部分Router轉(zhuǎn)發(fā)的工作。


    在設(shè)計層級架構(gòu)時,需要注意只能上層對下層依賴,下層對上層不能有依賴,下層中不要包含上層業(yè)務(wù)邏輯。對于項目中存在的公共資源和代碼,應(yīng)該將其下沉到下層中。


    為什么這么做?



    首先就像我剛才說的,我公司項目并不是很大,根本沒必要拆分的那么徹底。


    因為組件化開發(fā)有一個很重要的原因就是解耦合,如果我做到了底層不對上層依賴,這樣就已經(jīng)解除了上下層的相互耦合。而且上層對下層進(jìn)行調(diào)用的時候,也不是直接調(diào)用下層,通過一個接口類進(jìn)行中轉(zhuǎn),實現(xiàn)了下層的改變對上層無影響,這也是上層對下層解耦的表現(xiàn)。


    所以對于第三方就不用說了,上層直接調(diào)用下層的第三方也是沒問題的,這都是解耦的。


    模型類怎么辦,放在哪合適?



    casatwy對模型類的觀點是去Model化,簡單來說就是用字典代替Model存儲數(shù)據(jù)。這對于組件化架構(gòu)來說,是解決組件之間數(shù)據(jù)傳遞的一個很好的方法。


    因為模型類是關(guān)乎業(yè)務(wù)的,理論上必須放在業(yè)務(wù)層也就是業(yè)務(wù)組件這一層。但是要把模型對象從一個組件中當(dāng)做參數(shù)傳遞到另一個組件中,模型類放在調(diào)用方和服務(wù)方的哪個組件都不太合適,而且有可能不只兩個組件使用到這個模型對象。這樣的話在其他組件使用模型對象,必然會造成引用和耦合。


    那么如果把模型類放在Router中,這樣會造成Router耦合了業(yè)務(wù),造成業(yè)務(wù)的侵入性。如果在用到這個模型對象的所有組件中,都分別維護(hù)一份相同的模型類,這樣之后業(yè)務(wù)發(fā)生改變模型類就會很麻煩。


    那應(yīng)該怎么辦呢?



    如果將模型類單獨拉出來,定義一個模型組件呢?這個看起來比較可行,將這個定義模型的組件下沉到下層,模型組件不包含業(yè)務(wù),只聲明模型對象的類。但是一般組件的模型對象都是當(dāng)前組件內(nèi)使用的,將模型對象傳遞給其他組件的需求非常少,那所有的模型類都定義到模型組件嗎?


    對于這個問題,我建議在項目開發(fā)中將模型類還定義在當(dāng)前業(yè)務(wù)組件中,在組件間傳遞模型對象時進(jìn)行去Model化,傳遞字典類型的參數(shù)。

    上面只是思考,恰巧我公司持久化方案用的是CoreData,所有模型的定義都在CoreData組件中,這樣就避免了業(yè)務(wù)層組件之間因為模型類的耦合。


    滴滴組件化架構(gòu)



    之前看過滴滴iOS負(fù)責(zé)人李賢輝的技術(shù)分享,分享的是滴滴iOS客戶端的架構(gòu)發(fā)展歷程,下面簡單總結(jié)一下。


    發(fā)展歷程



    滴滴在最開始的時候架構(gòu)較混亂。然后在2.0時期重構(gòu)為MVC架構(gòu),使項目劃分更加清晰。在3.0時期上線了新的業(yè)務(wù)線,這時采用的游戲開發(fā)中的狀態(tài)機(jī)機(jī)制,暫時可以滿足現(xiàn)有業(yè)務(wù)。


    然而在后期不斷上線順風(fēng)車、代駕、巴士等多條業(yè)務(wù)線的情況下,現(xiàn)有架構(gòu)變得非常臃腫,代碼耦合嚴(yán)重。從而在2015年開始了代號為“The One”的方案,這套方案就是滴滴的組件化方案。


    架構(gòu)設(shè)計



    滴滴的組件化方案,和蘑菇街方案類似,也是通過私有CocoaPods來管理各個組件。將整個項目拆分為業(yè)務(wù)部分和技術(shù)部分,業(yè)務(wù)部分包括專車、拼車、巴士等業(yè)務(wù)模塊,每個業(yè)務(wù)模塊就是一個單獨的組件,使用一個pods管理。技術(shù)部分則分為登錄分享、網(wǎng)絡(luò)、緩存這樣的一些基礎(chǔ)組件,分別使用不同的pods管理。


    組件間通信通過ONERouter中間件進(jìn)行通信,ONERouter類似于MGJRouter,擔(dān)負(fù)起協(xié)調(diào)和調(diào)用各個組件的作用。組件間通信通過OpenURL方法,來進(jìn)行對應(yīng)的調(diào)用。ONERouter內(nèi)部保存一份Class-URL的映射表,通過URL找到Class并發(fā)起調(diào)用,Class的注冊放在+load方法中進(jìn)行。


    滴滴在組件內(nèi)部的業(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的代碼量就很少了。


    滴滴首頁分析



    滴滴文章中說道首頁只能有一個地圖實例,這在很多地圖導(dǎo)航相關(guān)應(yīng)用中都是這樣做的。滴滴首頁主控制器持有導(dǎo)航欄和地圖,每個業(yè)務(wù)線首頁控制器都添加在主控制器上,并且業(yè)務(wù)線控制器背景都設(shè)置為透明,將透明部分響應(yīng)事件傳遞到下面的地圖中,只響應(yīng)屬于自己的響應(yīng)事件。


    由主控制器來切換各個業(yè)務(wù)線首頁,切換頁面后根據(jù)不同的業(yè)務(wù)線來更新地圖數(shù)據(jù)。


    淘寶組件化架構(gòu)



    本章節(jié)源自于宗心在阿里技術(shù)沙龍上的一次分享

    https://yq.aliyun.com/articles/129


    架構(gòu)發(fā)展


    淘寶iOS客戶端初期是單工程的普通項目,但隨著業(yè)務(wù)的飛速發(fā)展,現(xiàn)有架構(gòu)并不能承載越來越多的業(yè)務(wù)需求,導(dǎo)致代碼間耦合很嚴(yán)重。后期開發(fā)團(tuán)隊對其不斷進(jìn)行重構(gòu),淘寶iOS和Android兩個平臺,除了某個平臺特有的一些特性或某些方案不便實施之外,大體架構(gòu)都是差不多的。


    發(fā)展歷程:



  • 剛開始是普通的單工程項目,以傳統(tǒng)的MVC架構(gòu)進(jìn)行開發(fā)。隨著業(yè)務(wù)不斷的增加,導(dǎo)致項目非常臃腫、耦合嚴(yán)重。

  • 2013年淘寶開啟“all in 無線”計劃,計劃將淘寶變?yōu)橐粋€大的平臺,將阿里系大多數(shù)業(yè)務(wù)都集成到這個平臺上,造成了業(yè)務(wù)的大爆發(fā)。

    淘寶開始實行插件化架構(gòu),將每個業(yè)務(wù)模塊劃分為一個組件,將組件以framework二方庫的形式集成到主工程。但這種方式并沒有做到真正的拆分,還是在一個工程中使用git進(jìn)行merge,這樣還會造成合并沖突、不好回退等問題。

  • 迎來淘寶移動端有史以來最大的重構(gòu),將其重構(gòu)為組件化架構(gòu)。將每個模塊當(dāng)做一個組件,每個組件都是一個單獨的項目,并且將組件打包成framework。主工程通過podfile集成所有組件framework,實現(xiàn)業(yè)務(wù)之間真正的隔離,通過CocoaPods實現(xiàn)組件化架構(gòu)。

  • 架構(gòu)優(yōu)勢



    淘寶是使用git來做源碼管理的,在插件化架構(gòu)時需要盡可能避免merge操作,否則在大團(tuán)隊中協(xié)作成本是很大的。而使用CocoaPods進(jìn)行組件化開發(fā),則避免了這個問題。


    在CocoaPods中可以通過podfile很好的配置各個組件,包括組件的增加和刪除,以及控制某個組件的版本。使用CocoaPods的原因,很大程度是為了解決大型項目中,代碼管理工具merge代碼導(dǎo)致的沖突。并且可以通過配置podfile文件,輕松配置項目。


    每個組件工程有兩個target,一個負(fù)責(zé)編譯當(dāng)前組件和運行調(diào)試,另一個負(fù)責(zé)打包framework。先在組件工程做測試,測試完成后再集成到主工程中集成測試。


    每個組件都是一個獨立app,可以獨立開發(fā)、測試,使得業(yè)務(wù)組件更加獨立,所有組件可以并行開發(fā)。下層為上層提供能滿足需求的底層庫,保證上層業(yè)務(wù)層可以正常開發(fā),并將底層庫封裝成framework集成到項目中。


    使用CocoaPods進(jìn)行組件集成的好處在于,在集成測試自己組件時,可以直接將本地主工程podfile文件中的當(dāng)前組件指向本地,就可以直接進(jìn)行集成測試,不需要提交到服務(wù)器倉庫。


    淘寶四層架構(gòu)





    淘寶四層架構(gòu)(圖片來自淘寶技術(shù)分享)


    淘寶架構(gòu)的核心思想是一切皆組件,將工程中所有代碼都抽象為組件。


    淘寶架構(gòu)主要分為四層,最上層是組件Bundle(業(yè)務(wù)組件),依次往下是容器(核心層),中間件Bundle(功能封裝),基礎(chǔ)庫Bundle(底層庫)。容器層為整個架構(gòu)的核心,負(fù)責(zé)組件間的調(diào)度和消息派發(fā)。


    總線設(shè)計



    總線設(shè)計:URL路由+服務(wù)+消息。統(tǒng)一所有組件的通信標(biāo)準(zhǔn),各個業(yè)務(wù)間通過總線進(jìn)行通信。



    總線設(shè)計(圖片來自淘寶技術(shù)分享)


    URL可以請求也可以接受返回值,和MGJRouter差不多。URL路由請求可以被解析就直接拿來使用,如果不能被解析就跳轉(zhuǎn)H5頁面。這樣就完成了一個對不存在組件調(diào)用的兼容,使用戶手中比較老的版本依然可以顯示新的組件。


    服務(wù)提供一些公共服務(wù),由服務(wù)方組件負(fù)責(zé)實現(xiàn),通過Protocol實現(xiàn)。消息負(fù)責(zé)統(tǒng)一發(fā)送消息,類似于通知也需要注冊。


    Bundle App




    Bundle App(圖片來自淘寶技術(shù)分享)


    淘寶提出Bundle App的概念,可以通過已有組件,進(jìn)行簡單配置后就可以組成一個新的app出來。解決了多個應(yīng)用業(yè)務(wù)復(fù)用的問題,防止重復(fù)開發(fā)同一業(yè)務(wù)或功能。


    Bundle即App,容器即OS,所有Bundle App被集成到OS上,使每個組件的開發(fā)就像app開發(fā)一樣簡單。這樣就做到了從巨型app回歸普通app的輕盈,使大型項目的開發(fā)問題徹底得到了解決。


    總結(jié)

    留個小思考

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

    我們是一群熱愛IT的年輕人,如果你也愛IT、愛移動端開發(fā),歡迎加入我們,讓我們共同為夢想發(fā)聲。

    關(guān)注藍(lán)鷗,推送IT新知識與資訊,讓你每天進(jìn)步一點點。

    PS:喜歡你就點個贊,有用你就收進(jìn)后宮,認(rèn)識程序員你就轉(zhuǎn)發(fā)一下辣。

    最后編輯于
    ?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
    【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
    平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

    相關(guān)閱讀更多精彩內(nèi)容

    友情鏈接更多精彩內(nèi)容