組件化架構漫談

該文章屬於<簡書 — 劉小壯>原創,轉載請註明:

<簡書 — 劉小壯> http://www.jianshu.com/p/67a6004f6930html


前段時間公司項目打算重構, 準確來講應該是按以前的產品邏輯重寫一個項目😂。在重構項目以前涉及到架構選型的問題,我和組裏小夥伴一塊兒研究了一下組件化架構, 打算將項目重構爲組件化架構。固然不是直接拿來照搬,仍是要根據公司具體的業務需求設計架構。

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

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


博客配圖

組件化架構的由來

隨着移動互聯網的不斷髮展,不少程序代碼量和業務愈來愈多,現有架構已經不適合公司業務的發展速度了,不少都面臨着重構的問題。github

在公司項目開發中,若是項目比較小,普通的單工程+MVC架構就能夠知足大多數需求了。可是像淘寶、蘑菇街、微信這樣的大型項目,原有的單工程架構就不足以知足架構需求了。web

就拿淘寶來講,淘寶在13年開啓的「All in 無線」戰略中,就將阿里系大多數業務都加入到手機淘寶中,使客戶端出現了業務的爆發。在這種狀況下,單工程架構則已經遠遠不能知足現有業務需求了。因此在這種狀況下,淘寶在13年開啓了插件化架構的重構,後來在14年迎來了手機淘寶有史以來最大規模的重構,將項目重構爲組件化架構算法

蘑菇街的組件化架構

緣由

在一個項目愈來愈大,開發人員愈來愈多的狀況下,項目會遇到不少問題。sql

  • 業務模塊間劃分不清晰,模塊之間耦合度很大,很是難維護。
  • 全部模塊代碼都編寫在一個項目中,測試某個模塊或功能,須要編譯運行整個項目

耦合嚴重的工程

爲了解決上面的問題,能夠考慮加一個中間層來協調各個模塊間的調用,全部的模塊間的調用都會通過中間層中轉。數據庫

中間層設計

可是發現增長這個中間層後,耦合仍是存在的。中間層對被調用模塊存在耦合,其餘模塊也須要耦合中間層才能發起調用。這樣仍是存在以前的相互耦合的問題,並且本質上比以前更麻煩了。編程

架構改進

因此應該作的是,只讓其餘模塊對中間層產生耦合關係,中間層不對其餘模塊發生耦合
對於這個問題,能夠採用組件化的架構,將每一個模塊做爲一個組件。而且創建一個主項目,這個主項目負責集成全部組件。這樣帶來的好處是不少的:設計模式

  • 業務劃分更佳清晰,新人接手更佳容易,能夠按組件分配開發任務。
  • 項目可維護性更強,提升開發效率。
  • 更好排查問題,某個組件出現問題,直接對組件進行處理。
  • 開發測試過程當中,能夠只編譯本身那部分代碼,不須要編譯整個項目代碼。
  • 方便集成,項目須要哪一個模塊直接經過CocoaPods集成便可。

改進後的架構

進行組件化開發後,能夠把每一個組件當作一個獨立的app,每一個組件甚至能夠採起不一樣的架構,例如分別使用MVVMMVCMVCS等架構,根據本身的編程習慣作選擇。

MGJRouter方案

蘑菇街經過MGJRouter實現中間層,由MGJRouter進行組件間的消息轉發,從名字上來講更像是「路由器」。實現方式大體是,在提供服務的組件中提早註冊block,而後在調用方組件中經過URL調用block,下面是調用方式。

架構設計

MGJRouter組件化架構

MGJRouter是一個單例對象,在其內部維護着一個「URL -> block」格式的註冊表,經過這個註冊表來保存服務方註冊的block,以及使調用方能夠經過URL映射出block,並經過MGJRouter對服務方發起調用。

MGJRouter是全部組件的調度中心,負責全部組件的調用、切換、特殊處理等操做,能夠用來處理一切組件間發生的關係。除了原生頁面的解析外,還能夠根據URL跳轉H5頁面。

在服務方組件中都對外提供一個PublicHeader,在PublicHeader中聲明當前組件所提供的全部功能,這樣其餘組件想知道當前組件有什麼功能,直接看PublicHeader便可。每個block都對應着一個URL,調用方能夠經過URLblock發起調用。

#ifndef UserCenterPublicHeader_h
#define UserCenterPublicHeader_h

/** 跳轉用戶登陸界面 */
static const NSString * CTBUCUserLogin = @"CTB://UserCenter/UserLogin";
/** 跳轉用戶註冊界面 */
static const NSString * CTBUCUserRegister = @"CTB://UserCenter/UserRegister";
/** 獲取用戶狀態 */
static const NSString * CTBUCUserStatus = @"CTB://UserCenter/UserStatus";

#endif

在組件內部實現block的註冊工做,以及block對外提供服務的代碼實現。在註冊的時候須要注意註冊時機,應該保證調用時URL對應的block已經註冊。

蘑菇街項目使用git做爲版本控制工具,將每一個組件都當作一個獨立工程,並創建主項目來集成全部組件。集成方式是在主項目中經過CocoaPods來集成,將全部組件當作二方庫集成到項目中。詳細的集成技術點在下面「標準組件化架構設計」章節中會講到。

MGJRouter調用

下面代碼模擬對詳情頁的註冊、調用,在調用過程當中傳遞id參數。參數傳遞能夠有兩種方式,相似於Get請求URL後面拼接參數,以及經過字典傳遞參數。下面是註冊的示例代碼:

[MGJRouter registerURLPattern:@"mgj://detail" 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和參數,AndroidiOS都使用這一套URL,能夠保持統一性。

基礎組件

在項目中存在不少公共部分的東西,例如封裝的網絡請求、緩存、數據處理等功能,以及項目中所用到的資源文件。蘑菇街將這些部分也當作組件,劃分爲基礎組件,位於業務組件下層。全部業務組件都使用同一套基礎組件,也能夠保證公共部分的統一性。

Protocol方案

總體架構

Protocol方案的中間件

爲了解決MGJRouter方案中 URL硬編碼 ,以及 字典參數類型不明確 等問題,蘑菇街在原有組件化方案的基礎上推出了Protocol方案。Protocol方案由兩部分組成,進行組件間通訊的ModuleManager類以及MGJComponentProtocol協議類。

經過中間件ModuleManager進行消息的調用轉發,在ModuleManager內部維護一張映射表,映射表由以前的"URL -> block"變成"Protocol -> Class"

在中間件中建立MGJComponentProtocol文件,服務方組件將能夠用來調用的方法都定義在Protocol中,將全部服務方的Protocol都分別定義到MGJComponentProtocol文件中,若是協議比較多也能夠分開幾個文件定義。這樣全部調用方依然是隻依賴中間件,不須要依賴除中間件以外的其餘組件。

Protocol方案中每一個組件須要一個MGJModuleImplement,此類負責實現當前組件對應的協議方法,也就是對外提供服務的實現。在程序開始運行時將自身的Class註冊到ModuleManager,並將Protocol反射爲字符串當作key

Protocol方案依然須要提早註冊服務,因爲Protocol方案是返回一個Class,並將Class反射爲對象再調用方法,這種方式不會直接調用類的內部邏輯。能夠將Protocol方案的Class註冊,都放在類對應的MGJModuleImplement中,或者專門創建一個RegisterProtocol類。

示例代碼

建立MGJUserImpl類當作User組件對外公開的類,並在MGJComponentProtocol.h中定義MGJUserProtocol協議,由MGJUserImpl類實現協議中定義的方法,完成對外提供服務的過程。下面是協議定義:

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

Class遵照協議並實現定義的方法,外界經過Protocol獲取的Class並實例化爲對象,調用服務方實現的協議方法。

ModuleManager的協議註冊方法,註冊時將Protocol反射爲字符串當作存儲的key,將實現協議的Class當作值存儲。經過ProtocolClass的時候,就是經過ProtocolModuleManager中將Class映射出來。

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

調用時經過ProtocolModuleManager中映射出註冊的Class,將獲取到的Class實例化,並調用Class實現的協議方法完成服務調用。

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

項目調用流程

蘑菇街是MGJRouterProtocol混用的方式,兩種實現的調用方式不一樣,但大致調用邏輯和實現思路相似。在MGJRouter不能知足需求或調用不方便時,就能夠經過Protocol的方式調用。

  1. 在進入程序後,先使用MGJRouter對服務方組件進行註冊。每一個URL對應一個block的實現,block中的代碼就是組件對外提供的服務,調用方能夠經過URL調用這個服務。
  2. 調用方經過MGJRouter調用openURL:方法,並將被調用代碼對應的URL傳入,MGJRouter會根據URL查找對應的block實現,從而調用組件的代碼進行通訊。
  3. 調用和註冊block時,block有一個字典用來傳遞參數。這樣的優點就是參數類型和數量理論上是不受限制的,可是須要不少硬編碼的key名在項目中。

內存管理

蘑菇街組件化方案有兩種,ProtocolMGJRouter的方式,但都須要進行register操做。Protocol註冊的是ClassMGJRouter註冊的是Block,註冊表是一個NSMutableDictionary類型的字典,而字典的擁有者又是一個單例對象,這樣會形成內存的常駐

下面是對兩種實現方式內存消耗的分析:

  • 首先說一下MGJRouter方案可能致使的內存問題,因爲block會對代碼塊內部對象進行持有,若是使用不當很容易形成內存泄漏的問題。
    block自身實際上不會形成很大的內存泄漏,主要是內部引用的變量,因此在使用時就須要注意強引用的問題,並適當使用weak修飾對應的變量。以及在適當的時候,釋放對應的變量。
    除了對外部變量的引用,在block代碼塊內部儘可能不要直接建立對象,應該經過方法調用中轉一下。
  • 對於協議這種實現方式,和block內存常駐方式差很少。只是將存儲的block對象換成Class對象。這其實是存儲的類對象,類對象原本就是單例模式,因此不會形成多餘內存佔用。

casatwy組件化方案

總體架構

casatwy組件化方案能夠處理兩種方式的調用,遠程調用和本地調用,對於兩個不一樣的調用方式分別對應兩個接口。

  • 遠程調用經過AppDelegate代理方法傳遞到當前應用後,調用遠程接口並在內部作一些處理,處理完成後會在遠程接口內部調用本地接口,以實現本地調用爲遠程調用服務
  • 本地調用由performTarget:action:params:方法負責,但調用方通常不直接調用performTarget:方法CTMediator會對外提供明確參數和方法名的方法,在方法內部調用performTarget:方法和參數的轉換。

casatwy提出的組件化架構

架構設計思路

casatwy是經過CTMediator類實現組件化的,在此類中對外提供明確參數類型的接口,接口內部經過performTarget方法調用服務方組件的TargetAction。因爲CTMediator類的調用是經過runtime主動發現服務的,因此服務方對此類是徹底解耦的。

但若是CTMediator類對外提供的方法都放在此類中,將會對CTMediator形成極大的負擔和代碼量。解決方法就是對每一個服務方組件建立一個CTMediatorCategory,並將對服務方的performTarget調用放在對應的Category中,這些Category都屬於CTMediator中間件,從而實現了感官上的接口分離。

casatwy組件化實現細節

對於服務方的組件來講,每一個組件都提供一個或多個Target類,在Target類中聲明Action方法。Target類是當前組件對外提供的一個「服務類」,Target將當前組件中全部的服務都定義在裏面,CTMediator經過runtime主動發現服務

Target中的全部Action方法,都只有一個字典參數,因此能夠傳遞的參數很靈活,這也是casatwy提出的Model化的概念。在Action的方法實現中,對傳進來的字典參數進行解析,再調用組件內部的類和方法。

架構分析

casatwy爲咱們提供了一個Demo,經過這個Demo能夠很好的理解casatwy的設計思路,下面按照個人理解講解一下這個Demo

文件目錄

打開Demo後能夠看到文件目錄很是清楚,在上圖中用藍框框出來的就是中間件部分,紅框框出來的就是業務組件部分。我對每一個文件夾作了一個簡單的註釋,包含了其在架構中的職責。

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

// 遠程App調用入口
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;
// 本地組件調用入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;

CTMediator中定義的ModuleACategory,爲其餘組件提供了一個獲取控制器並跳轉的功能,下面是代碼實現。因爲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的方式調用,這個過程就叫作發現服務

Target_A中對傳遞的參數作了處理,以及內部的業務邏輯實現。方法是發生在ModuleA內部的,這樣就能夠保證組件內部的業務不受外部影響,對內部業務沒有侵入性

- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params {
    // 對傳過來的字典參數進行解析,並調用ModuleA內部的代碼
    DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
    viewController.valueLabel.text = params[@"key"];
    return viewController;
}

命名規範

在大型項目中代碼量比較大,須要避免命名衝突的問題。對於這個問題casatwy採起的是加前綴的方式,從casatwyDemo中也能夠看出,其組件ModuleATarget命名爲Target_A,能夠區分各個組件的Target。被調用的Action命名爲Action_nativeFetchDetailViewController:,能夠區分組件內的方法與對外提供的方法。

casatwy將類和方法的命名,都統一按照其功能作區分當作前綴,這樣很好的將組件相關和組件內部代碼進行了劃分。

結果分析

Protocol

從我調研和使用的結果來講,並不推薦使用Protocol方案。首先Protocol方案的代碼量就比MGJRouter方案的要多,調用和註冊代碼量很大,調用起來並非很方便。

本質上來講Protocol方案是經過類對象實例一個變量,並調用變量的方法,並無真正意義上的改變組件之間的交互方案,但MGJRouter的方案卻經過URL Router的方式改變和統一了組件間調用方式。

而且Protocol沒有對Remote Router的支持,不能直接處理來自Push的調用,在靈活性上就不如MGJRouter的方案。

CTMediator

我並不推薦CTMediator方案,這套方案其實是一套很臃腫的方案。雖然爲CTMediator提供了不少Category但實際上組件間的調用邏輯都耦合在了中間件中。一樣,和Protocol方案存在一個相同的問題,就是調用代碼量很大,使用起來並不方便。

CTMediator方案中存在不少硬編碼的問題,例如targetaction以及參數名都是硬編碼在中間件中的,這種調用方式並不靈活直接。

casatwy提出了去Model化的想法,我以爲這在組件化中傳參來講,是很是靈活的,這點我比較認同。相對於MGJRouter的話,也採用了去Model化的傳參方式,而不是直接傳遞模型對象。組件化傳參並不適用傳模型對象,但組件內部仍是可使用Model的。

MGJRouter

MGJRouter方案是一套很是輕量級的方案,其中間件代碼總共也就兩百行之內,很是簡潔。在調用時直接經過URL調用,調用起來很簡單,我推薦使用這套方案做爲組件化架構的中間件。

MGJRouter最強大的一點在於,統一了遠程調用和本地調用。這就使得能夠經過Push的方式,進行任何容許的組件間調用,對項目運營是有很大幫助的。

這三套方案都實現了組件間的解耦,MGJRouterProtocol都是調用方對中間件的耦合,CTMediator是中間件對組件的耦合,都是單向耦合。

接口類

在三套方案中,服務方組件都對外提供一個PublicHeaderTarget在文件中統必定義對外提供的服務,組件間通訊的實現代碼大多數都在裏面。

但三套實現方案實現方式並不一樣,蘑菇街的兩套方案都須要註冊操做,不管是Block仍是Protocol都須要註冊後才能夠提供服務。而casatwy的方案則不須要,直接經過runtime調用。

組件化架構設計

在上面文章中提到了casatwy方案的CTMediator,蘑菇街方案的MGJRouterModuleManager,以後將統稱爲中間件,下面讓咱們設計一套組件化架構。

總體架構

組件化架構中,須要一個主工程,主工程負責集成全部組件。每一個組件都是一個單獨的工程,建立不一樣的git私有倉庫來管理,每一個組件都有對應的開發人員負責開發。開發人員只須要關注與其相關組件的代碼,不用考慮其餘組件,這樣來新人也好上手。

組件的劃分須要注意組件粒度,粒度根據業務可大可小。組件劃分能夠將每一個業務模塊都劃分爲組件,對於網絡、數據庫等基礎模塊,也應該劃分到組件中。項目中會用到不少資源文件、配置文件等,也應該劃分到對應的組件中,避免重複的資源文件。項目實現徹底的組件化。

每一個組件都須要對外提供調用,在對外公開的類或組件內部,註冊對應的URL。組件處理中間件調用的代碼應該對其餘代碼無侵入,只負責對傳遞過來的數據進行解析和組件內調用的功能。

組件集成

組件化集成

每一個組件都是一個單獨的工程,在組件開發完成後上傳到git倉庫。主工程經過Cocoapods集成各個組件,集成和更新組件時只須要pod update便可。這樣就是把每一個組件當作第三方來管理,管理起來很是方便。

Cocoapods能夠控制每一個組件的版本,例如在主項目中回滾某個組件到特定版本,就能夠經過修改podfile文件實現。選擇Cocoapods主要由於其自己功能很強大,能夠很方便的集成整個項目,也有利於代碼的複用。經過這種集成方式,能夠很好的避免在傳統項目中代碼衝突的問題

集成方式

對於組件化架構的集成方式,我在看完bang的博客後專門請教了一下bang。根據在微博上和bang的聊天以及其餘博客中的學習,在主項目中集成組件主要分爲兩種方式——源碼和framework,但都是經過CocoaPods來集成。

不管是用CocoaPods管理源碼,仍是直接管理framework,集成方式都是同樣的,都是直接進行pod updateCocoaPods操做。

這兩種組件集成方案,實踐中也是各有利弊。直接在主工程中集成代碼文件,能夠看到其內部實現源碼,方便在主工程中進行調試。集成framework的方式,能夠加快編譯速度,並且對每一個組件的代碼有很好的保密性。若是公司對代碼安全比較看重,能夠考慮framework的形式。

例如手機QQ或者支付寶這樣的大型程序,通常都會採起framework的形式。並且通常這樣的大公司,都會有本身的組件庫,這個組件庫每每能夠表明一個大的功能或業務組件,直接添加項目中就可使用。關於組件化庫在後面講淘寶組件化架構的時候會提到。

資源文件

對於項目中圖片的集成,能夠把圖片當作一個單獨的組件,組件中只存在圖片文件,沒有任何代碼。圖片可使用Bundleimage assets進行管理,若是是Bundle就針對不一樣業務模塊創建不一樣的Bundle,若是是image assets,就按照不一樣的模塊分類創建不一樣的assets,將全部資源放在同一個組件內。

Bundleimage assets二者相比,我仍是更推薦用assets的方式,由於assets自身提供不少功能(例如設置圖片拉伸範圍),並且在打包以後圖片會被打包在.cer文件中,不會被看到。(如今也能夠經過工具對.cer文件進行解析,獲取裏面的圖片)

使用Cocoapods,全部的資源文件都放置在一個podspec中,主工程能夠直接引用這個podspec,假設此podspec名爲:Assets,而這個Assetspodspec裏面配置信息能夠寫爲:

s.resources = "Assets/Assets.xcassets/ ** / *.{png}"

主工程則直接在podfile文件中加入:

pod 'Assets', :path => '../MainProject/Assets'(這種寫法是訪問本地的,能夠換成git)

這樣便可在主工程直接訪問到Assets中的資源文件(不侷限圖片,sqlitejshtml亦可,在s.resources設置好配置信息便可)了。

優勢

  • 組件化開發能夠很好的提高代碼複用性,組件能夠直接拿到其餘項目中使用, 這個優勢在下面淘寶架構中會着重講一下。
  • 對於調試工做,能夠放在每一個組件中完成。單獨的業務組件能夠直接提交給測試使用,這樣測試起來也比較方便。最後組件開發完成並測試經過後,再將全部組件更新到主項目,提交給測試進行集成測試便可。
  • 經過這樣的組件劃分,組件的開發進度不會受其餘業務的影響,能夠多個組件並行開發。組件間的通訊都交給中間件來進行,須要通訊的類只須要接觸中間件,而中間件不須要耦合其餘組件,這就實現了組件間的解耦。中間件負責處理全部組件之間的調度,在全部組件之間起到控制核心的做用
  • 組件化框架清晰的劃分了不一樣模塊,從總體架構上來約束開發人員進行組件化開發,實現了組件間的物理隔離。組件化架構在各個模塊之間自然造成了一道屏障,避免某個開發人員偷懶直接引用頭文件,產生組件間的耦合,破壞總體架構。
  • 使用組件化架構進行開發時,由於每一個人都負責本身的組件,代碼提交也只提交本身負責模塊的倉庫,因此代碼衝突的問題會變得不多
  • 假設之後某個業務發生大的改變,須要對相關代碼進行重構,能夠在單個組件內進行重構。組件化架構下降了重構的風險,保證了代碼的健壯性。

架構分析

MGJRouter方案中,是經過調用OpenURL:方法並傳入URL來發起調用的。鑑於URL協議名等固定格式,能夠經過判斷協議名的方式,使用配置表控制H5native的切換配置表能夠從後臺更新,只須要將協議名更改一下便可。

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

假設如今線上的native組件出現嚴重bug在後臺將配置文件中原有的本地URL換成H5URL,並更新客戶端配置文件

在調用MGJRouter時傳入這個H5URL便可完成切換,MGJRouter判斷若是傳進來的是一個H5URL就直接跳轉webView。並且URL能夠傳遞參數給MGJRouter,只須要MGJRouter內部作參數截取便可。

使用組件化架構開發,組件間的通訊都是有成本的。因此儘可能將業務封裝在組件內部,對外只提供簡單的接口。即「高內聚、低耦合」原則

把握好組件劃分粒度的細化程度,太細則項目過於分散,太大則項目組件臃腫。可是項目都是從小到大的一個發展過程,因此不斷進行重構是掌握這個組件的細化程度最好的方式

注意點

若是經過framework等二進制形式,將組件集成到主項目中,須要注意預編譯指令的使用。由於預編譯指令在打包framework的時候,就已經在組件二進制代碼中打包好,到主項目中的時候預編譯指令其實已經再也不起做用了,而是已經在打包時按照預編譯指令編碼爲固定二進制。

我公司架構

對於項目架構來講,必定要創建於業務之上來設計架構。不一樣的項目業務不一樣,組件化方案的設計也會不一樣,應該設計最適合公司業務的架構。

架構設計

我公司項目是一個地圖導航應用,業務層之下的核心模塊和基礎模塊佔比較大,涉及到地圖SDK、算路、語音等模塊。且基礎模塊相對比較獨立,對外提供了不少調用接口。由此能夠看出,公司項目是一個重邏輯的項目,不像電商等App偏展現。

項目總體的架構設計是:層級架構+組件化架構,對於具體的實現細節會在下面詳細講解。採起這種結構混合的方式進行總體架構,對於組件的管理和層級劃分比較有利,符合公司業務需求。

公司組件化架構

在設計架構時,咱們將整個項目都拆分爲組件,組件化程度至關高。用到哪一個組件就在工程中經過Podfile進行集成,並經過URLRouter統一全部組件間的通訊。

組件化架構是項目的總體框架,而對於框架中每一個業務模塊的實現,能夠是任意方式的架構,MVVMMVCMVCS等都是能夠的,只要經過MGJRouter將組件間的通訊方式統一便可。

分層架構

組件化架構在物理結構上來講是不分層次的,只有組件與組件之間的劃分關係。可是在組件化架構的基礎上,應該根據項目和業務設計本身的層次架構,這套層次架構能夠用來區分組件所處的層次及職責,因此咱們設計了層級架構+組件化架構的總體架構。

我公司項目最開始設計的是三層架構:業務層 -> 核心層 (high + low) -> 基礎層,其中核心層又分爲highlow兩部分。可是這種架構會形成核心層太重,基礎層太輕的問題,這種並不適合組件化架構。

在三層架構中會發現,low層並無耦合業務邏輯,在同層級中是比較獨立的,職責較爲單一和基礎。咱們對low層下沉到基礎層中,並和基礎層進行合併。因此架構被從新分爲三層架構:業務層 -> 核心層 -> 基礎層。以前基礎層大可能是資源文件和配置文件,在項目中存在感並不高。

在分層架構中,須要注意只能上層對下層依賴,下層對上層不能有依賴,下層中不要包含上層業務邏輯。對於項目中存在的公共資源和代碼,應該將其下沉到下層中。

職責劃分

在三層架構中,業務層負責處理上層業務,將不一樣業務劃分到相應組件中,例如IM組件、導航組件、用戶組件等。業務層的組件間關係比較複雜,會涉及到組件間業務的通訊,以及業務層組件對下層組件的引用。

核心層位於業務層下方,爲業務層提供業務支持,如網絡、語音識別等組件應該劃分到核心層。核心層應該儘可能減小組件間的依賴,將依賴降到最小。核心層有時相互之間也須要支持,例如經緯度組件須要網絡組件提供網絡請求的支持,這種是不可避免的。

其餘比較基礎的模塊,都放在基礎層當作基礎組件。例如AFN、地圖SDK、加密算法等,這些組件都比較獨立且不摻雜任何業務邏輯,職責更加單一,相對於核心層更底層。能夠包含第三方庫、資源文件、配置文件、基礎庫等幾大類,基礎層組件相互之間不該該產生任何依賴。

在設計各個組件時,應該遵循「高內聚,低耦合」的設計規範,組件的調用應該簡單且直接,減小調用方的其餘處理。 對於核心層和基礎層的劃分,能夠以是否涉及業務、是否涉及同級組件間通訊、是否常常改動爲參照點。若是符合這幾點則放在覈心層,若是不符合則放在基礎層。

集成方式

新建一個項目後,首先將配置文件、URLRouterApp容器等集成到主工程中,作一些基礎的項目配置,隨後集成須要的組件便可。項目被總體拆分爲組件化架構後,應用對全部組件的集成方式都是同樣的,經過Podfile將須要的組件集成到項目中。經過組件化的方式,使得開發新項目速度變得很是快。

在集成業務層和核心層組件後,組件間的通訊都是由URLRouter進行通訊,項目中不容許直接依賴組件源碼。而基礎層組件則在集成後直接依賴,例如資源文件和配置文件,這些都是直接在主工程或組件中使用的。第三方庫則是經過核心層的業務封裝,封裝後由URLRouter進行通訊,但核心層也是直接依賴第三方庫源碼的。

組件的集成方式有兩種,源碼和framework的形式,咱們使用framework的方式集成。由於通常都是項目比較大才用組件化的,但大型項目都會存在編譯時間的問題,若是經過framework則會大大減小編譯時間,能夠節省開發人員的時間。

組件間通訊

對於組件間通訊,咱們採用的MGJRouter方案。由於MGJRouter如今已經很穩定了,並且能夠知足蘑菇街這樣量級的App需求,證實是很好的,不必本身寫一套再慢慢踩坑。

MGJRouter的好處在於,其調用方式很靈活,經過MGJRouter註冊並在block中處理回調,經過URL直接調用或者URL+Params字典的方式進行調用。因爲經過URL拼接參數或Params字典傳值,因此其參數類型沒有數量限定,傳遞比較靈活。在經過openURL:調用後,能夠在completionBlock中處理完成邏輯。

MGJRouter有個問題在於,在編寫組件間通訊的代碼時,會涉及到大量的Hardcode。對於Hardcode的問題,蘑菇街開發了一套後臺系統,將全部的Router須要的URL和參數名,都定義到這套系統中。咱們維護了一個Plist表,內部按不一樣組件進行劃分,包含URL和傳參名以及回調參數。

組件Router表

路由層安全

組件化架構須要注意路由層的安全問題。MGJRouter方案能夠處理本地及遠程的OpenURL調用,若是是程序內組件間的OpenURL調用,則不須要進行校驗。而跨應用的OpenURL調用,則須要進行合法性檢查。這是爲了防止第三方僞造進行OpenURL調用,因此對應用外調起的OpenURL進行的合法性檢查,例如其餘應用調起、服務器Remote Push等。

在合法性檢查的設計上,每一個從應用外調起的合法URL都會帶有一個token,在本地會對token進行校驗。這種方式的優點在於,沒有網絡請求的限制和延時。

代理方法

在項目中常常會用到代理模式傳值,代理模式在iOS中主要分爲三部分,協議、代理方、委託方三部分。

代理設計模式

但若是使用組件化架構的話,會涉及到組件與組件間的代理傳值,代理方須要設置爲委託方的delegate,但組件間是不能夠直接產生耦合的。對於這種跨組件的代理狀況,咱們直接將代理方的對象經過MGJRouter以參數的形式傳給另外一個組件,在另外一個組件中進行代理設置。

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;
}];

協議的定義放在委託方組件的PublicHeader.h中,代理方組件只引用這個PublicHeader.h文件,不耦合委託方內部代碼。爲了不定義的代理方法中出現耦合的狀況,方法中不能出現和組件內部業務有關的對象,只能傳遞系統的類。若是涉及到交互的狀況,則經過協議方法的返回值進行。

組件傳參

MGJRouter能夠在openURL:時傳入一個NSDictionary參數,在接觸RAC以後,我在想是否是能夠把NSDictionary參數變爲RACSignal參數,直接傳一個信號過去

註冊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方式優勢在於,相對於直接傳字典過去更加靈活,而且具有RAC的諸多特性。但缺點也很多,信號控制很差亂用的話也很容易挖坑,是否使用仍是看團隊狀況了。

常量定義

在項目中常常會定義一些常量,例如通知名、常量字符串等,這些常量通常都和所屬組件有很強的關係,很差單獨拆出來放到其餘組件。可是這些變量數量並非不少,並且不是每一個組件中都有。

因此,咱們將這些變量都聲明在PublicHeader.h文件中,其餘組件只能引用PublicHeader.h文件,不能引用組件內部業務代碼,這樣就規避掉了組件間耦合的問題。

H5和Native通訊

在項目中常常會用到H5頁面,若是能經過點擊H5頁面調起原生頁面,這樣的話NativeH5的融合會更好。因此咱們設計了一套H5Native交互的方案,這套方案可使用URLRouter的方式調起原生頁面,實現方式也很簡單,而且這套方案和H5本來的跳轉邏輯並不衝突。

經過iOS自帶UIWebView建立一個H5頁面後,H5能夠經過調用下面的JS函數和Native通訊。調用時能夠傳入新的URL,這個URL能夠設置爲URLRouterURL

window.location.href = 'CTB://UserCenter/UserLogin?userName=lxz&WeChatID=lz2046703959';

經過JS刷新H5頁面時,會調用下面的代理方法。若是方法返回YES,則會根據URL協議進行跳轉。

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

跳轉時系統會判斷通訊協議,若是是HTTP等標準協議,則會在當前頁面進行刷新。若是跳轉協議在URL Schame中註冊,則會經過系統openURL:的方式調用到AppDelegate的系統代理方法中,在代理方法中調用URLRouter,則能夠經過H5頁面喚起原生頁面。

AppService

在應用啓動過程當中,一般會作一些初始化操做。有些初始化操做是運行程序所須要的,例如崩潰統計、創建服務器的長鏈接等。或有的組件會對初始化操做有依賴關係,例如網絡組件依賴requestToken等。

對於應用啓動時的初始化操做,應該建立一個AppService來統一管理啓動操做,將初始化操做都放在裏面,包含建立根控制器等。其中有的初始化操做須要儘快執行,有的並不須要當即執行,能夠根據不一樣操做設定優先級,來管理全部初始化操做。

#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存儲數據。這對於組件化架構來講,是解決組件之間數據傳遞的一個很好的方法。可是去Model的方式,會存在大量的字段讀取代碼,使用起來遠沒有模型類方便。

由於模型類是關乎業務的,理論上必須放在業務層也就是業務組件這一層。可是要把模型對象從一個組件中當作參數傳遞到另外一個組件中,模型類放在調用方和被調方的哪一個組件都不太合適,並且有可能不僅兩個組件使用到這個模型對象。這樣的話在其餘組件使用模型對象,必然會形成引用和耦合

若是在用到這個模型對象的全部組件中,都分別維護一份相同的模型類,或者各自維護不一樣結構的模型類,這樣以後業務發生改變模型類就會很麻煩,這是不可取的。

設計方案
若是將全部模型類單獨拉出來,定義一個模型組件呢?

這個看起來比較可行,將這個定義模型的組件下沉到基礎層,模型組件不包含業務,只聲明模型對象的類。 若是將原來各個組件的模型類定義都拉出來,單獨放在一個組件中,能夠將原有各組件的Model層變得很輕量,這樣對整個項目架構來講也是有好處的。

在經過Router進行組件間調用時,經過字典進行傳值,這種方式比較靈活。在組件內部使用Model層時,仍是用模型組件中定義的Model類。Model層建議仍是用Model對象的形式比較方便,不建議總體使用去Model化的設計。在接收到其餘組件傳遞過來的字典參數時,能夠經過Model類提供的初始化方法,或其餘轉Model框架將字典轉爲Model對象。

@interface CTBStoreWelfareListModel : NSObject
/**
 * 自定義初始化方法
 */
- (instancetype)initWithDict:(NSDictionary *)dict;
@end

我公司持久化方案用的是CoreData,全部模型的定義都在CoreData組件中,則不須要再單首創建一個模型組件。

動態化構想

我公司項目是一個常規的地圖類項目,首頁和百度、高德等主流地圖導航App同樣,有不少添加在地圖上的控件。有的版本會添加控件上去,而有的版本會刪除控件,與之對應的功能也會被隱藏。

因此,有次和組裏小夥伴們開會的時候就在考慮, 能不能在服務器下發代碼對首頁進行佈局! 這樣就能夠對首頁進行動態佈局,例若有活動的時候在指定時間顯示某個控件,這樣能夠避免App Store審覈慢的問題。又或者線上某個模塊出現問題,能夠緊急下架出問題的模塊。

對於這個問題,咱們設計了一套動態配置方案,這套方案能夠對整個App進行配置。

配置表設計

對於動態配置的問題,咱們簡單設計了一個配置表,初期打算在首頁上先進行試水,之後可能會佈置到更多的頁面上。這樣應用程序各模塊的入口,均可以經過配置表來控制,而且經過Router控制頁面間跳轉,靈活性很是大。

在第一次安裝程序時使用內置的配置表,以後每次都用服務器來替換本地的配置表,這樣就能夠實現動態配置應用。下面是一個簡單設計的配置數據,JSON中配置的是首頁的配置信息,用來模擬服務器下發的數據,真正服務器下發的字段會比這個多不少。

{
    "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/***"
        }
    ]
}

對於服務器返回的數據,咱們會建立一套解析器,這個解析器用來將JSON解析並「轉換」爲標準的UIKit控件。點擊後的事件都經過Router進行跳轉,因此首頁的靈活性和Router的使用程度成正比

這套方案相似於React Native的方案,從服務器下發頁面展現效果,但沒有React Native功能那麼全。相對而言是一個輕量級的配置方案,主要用於頁面配置。

資源動態配置

除了頁面的配置以外,咱們發現地圖類App通常都存在ipa過大的問題,這樣在下載時很消耗流量以及時間。因此咱們就在想能不能把資源也作到動態配置,在用戶運行程序的時候再加載資源文件包。

咱們想經過配置表的方式,將圖片資源文件都放到服務器上,圖片的URL也隨配置表一塊兒從服務器獲取。在使用時請求圖片並緩存到本地,成爲真正的網絡APP。在此基礎上設計緩存機制,按期清理本地的圖片緩存,減小用戶磁盤佔用。

滴滴組件化架構

以前看過滴滴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客戶端初期是單工程的普通項目,但隨着業務的飛速發展,現有架構並不能承載愈來愈多的業務需求,致使代碼間耦合很嚴重。後期開發團隊對其不斷進行重構,將項目重構爲組件化架構,淘寶iOSAndroid兩個平臺,除了某個平臺特有的一些特性或某些方案不便實施以外,大致架構都是差很少的。

發展歷程

  1. 剛開始是普通的單工程項目,以傳統的MVC架構進行開發。隨着業務不斷的增長,致使項目很是臃腫、耦合嚴重。
  2. 2013年淘寶開啓 "all in 無線"計劃 ,計劃將淘寶變爲一個大的平臺,將阿里系大多數業務都集成到這個平臺上,形成了業務的大爆發

淘寶開始實行插件化架構,將每一個業務模塊劃分爲一個子工程,將組件以framework二方庫的形式集成到主工程。但這種方式並無作到真正的拆分,仍是在一個工程中使用git進行merge,這樣還會形成合並衝突、很差回退等問題。

  1. 迎來淘寶移動端有史以來最大的重構,將其重構爲組件化架構。將每一個模塊當作一個組件,每一個組件都是一個單獨的項目,而且將組件打包成framework。主工程經過podfile集成全部組件的framework,實現業務之間真正的隔離,經過CocoaPods實現組件化架構。

架構優點

淘寶是使用git來作源碼管理的,在插件化架構時須要儘量避免merge操做,不然在大團隊中協做成本是很大的。而使用CocoaPods進行組件化開發,則避免了這個問題。

CocoaPods中能夠經過podfile很好的配置各個組件,包括組件的增長和刪除,以及控制某個組件的版本。使用CocoaPods的緣由,很大程度是爲了解決大型項目中,代碼管理工具merge代碼致使的衝突。而且能夠經過配置podfile文件,輕鬆配置項目。

每一個組件工程有兩個target一個負責編譯當前組件和運行調試,另外一個負責打包framework。先在組件工程作測試,測試完成後再集成到主工程中集成測試。

每一個組件都是一個獨立app,能夠獨立開發、測試,使得業務組件更加獨立,全部組件能夠並行開發。下層爲上層提供能知足需求的底層庫,保證上層業務層能夠正常開發,並將底層庫封裝成framework集成到主工程中。

使用CocoaPods進行組件集成的好處在於,在集成測試本身組件時,能夠直接在本地主工程中,經過podfile使用當前組件源碼,能夠直接進行集成測試,不須要提交到服務器倉庫。

淘寶四層架構

淘寶四層架構(圖片來自淘寶技術分享)

淘寶架構的核心思想是一切皆組件,將工程中全部代碼都抽象爲組件。

淘寶架構主要分爲四層,最上層是組件Bundle(業務組件),依次往下是容器(核心層),中間件Bundle(功能封裝),基礎庫Bundle(底層庫)。容器層爲整個架構的核心,負責組件間的調度和消息派發。

總線設計

總線設計:URL路由+服務+消息。統一全部組件的通訊標準,各個業務間經過總線進行通訊。

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

URL總線

經過URL總線對三端進行了統一,一個URL能夠調起iOSAndroid、前端三個平臺,產品運營和服務器只須要下發一套URL便可調用對應的組件。

URL路由能夠發起請求也能夠接受返回值,和MGJRouter差很少。URL路由請求能夠被解析就直接拿來使用,若是不能被解析就跳轉H5頁面。這樣就完成了一個對不存在組件調用的兼容,使用戶手中比較老的版本依然能夠顯示新的組件。

服務提供一些公共服務,由服務方組件負責實現,經過Protocol進行調用。

消息總線

應用經過消息總線進行事件的中心分發,相似於iOS的通知機制。例如客戶端先後臺切換,則能夠經過消息總線分發到接收消息的組件。由於經過URLRouter只是一對一的進行消息派發和調度,若是屢次註冊同一個URL,則會被覆蓋掉。

Bundle App

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

在組件化架構的基礎上,淘寶提出Bundle App的概念,能夠經過已有組件,進行簡單配置後就能夠組成一個新的app出來。解決了多個應用業務複用的問題,防止重複開發同一業務或功能。

BundleApp,容器即OS,全部Bundle App被集成到OS上,使每一個組件的開發就像app開發同樣簡單。這樣就作到了從巨型app迴歸普通app的輕盈,使大型項目的開發問題完全獲得瞭解決。


總結

留個小思考

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

weixin://dl/profile

總結

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

個人博客地址

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

蘑菇街-MGJRouter
casatwy-CTMediator

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

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

組件化架構集成Demo

因爲簡書排版並非很好,因此作了一個PDF版的《組件化架構漫談》,放在我Github上了。PDF上有文章目錄,方便閱讀,下面是地址。

若是你以爲不錯,請把PDF幫忙轉到其餘羣裏,或者你的朋友,讓更多的人瞭解組件化架構,衷心感謝! 😁

組件化架構PDF

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

相關文章
相關標籤/搜索