<簡書 — 劉小壯> http://www.jianshu.com/p/67a6004f6930html
前段時間公司項目打算重構, 準確來講應該是按以前的產品邏輯重寫一個項目😂。在重構項目以前涉及到架構選型的問題,我和組裏小夥伴一塊兒研究了一下組件化架構, 打算將項目重構爲組件化架構。固然不是直接拿來照搬,仍是要根據公司具體的業務需求設計架構。在學習組件化架構的過程當中,從不少高質量的博客中學到很多東西,例如蘑菇街李忠、casatwy、bang的博客。在學習過程當中也遇到一些問題,在微博和QQ上和一些作
iOS
的朋友進行了交流,很是感謝這些朋友的幫助。前端本篇文章主要針對於以前蘑菇街提出的組件化方案,以及casatwy提出的組件化方案進行分析,後面還會簡單提到滴滴、淘寶、微信的組件化架構,最後會簡單說一下我公司設計的組件化架構。git
隨着移動互聯網的不斷髮展,不少程序代碼量和業務愈來愈多,現有架構已經不適合公司業務的發展速度了,不少都面臨着重構的問題。github
在公司項目開發中,若是項目比較小,普通的單工程+MVC架構
就能夠知足大多數需求了。可是像淘寶、蘑菇街、微信這樣的大型項目,原有的單工程架構就不足以知足架構需求了。web
就拿淘寶來講,淘寶在13年開啓的「All in 無線」
戰略中,就將阿里系大多數業務都加入到手機淘寶中,使客戶端出現了業務的爆發。在這種狀況下,單工程架構則已經遠遠不能知足現有業務需求了。因此在這種狀況下,淘寶在13年開啓了插件化架構的重構,後來在14年迎來了手機淘寶有史以來最大規模的重構,將項目重構爲組件化架構。算法
在一個項目愈來愈大,開發人員愈來愈多的狀況下,項目會遇到不少問題。sql
爲了解決上面的問題,能夠考慮加一個中間層來協調各個模塊間的調用,全部的模塊間的調用都會通過中間層中轉。數據庫
可是發現增長這個中間層後,耦合仍是存在的。中間層對被調用模塊存在耦合,其餘模塊也須要耦合中間層才能發起調用。這樣仍是存在以前的相互耦合的問題,並且本質上比以前更麻煩了。編程
因此應該作的是,只讓其餘模塊對中間層產生耦合關係,中間層不對其餘模塊發生耦合。
對於這個問題,能夠採用組件化的架構,將每一個模塊做爲一個組件。而且創建一個主項目,這個主項目負責集成全部組件。這樣帶來的好處是不少的:設計模式
CocoaPods
集成便可。
進行組件化開發後,能夠把每一個組件當作一個獨立的app,每一個組件甚至能夠採起不一樣的架構,例如分別使用MVVM
、MVC
、MVCS
等架構,根據本身的編程習慣作選擇。
蘑菇街經過MGJRouter
實現中間層,由MGJRouter
進行組件間的消息轉發,從名字上來講更像是「路由器」。實現方式大體是,在提供服務的組件中提早註冊block
,而後在調用方組件中經過URL
調用block
,下面是調用方式。
MGJRouter
是一個單例對象,在其內部維護着一個「URL -> block」
格式的註冊表,經過這個註冊表來保存服務方註冊的block
,以及使調用方能夠經過URL
映射出block
,並經過MGJRouter
對服務方發起調用。
MGJRouter
是全部組件的調度中心,負責全部組件的調用、切換、特殊處理等操做,能夠用來處理一切組件間發生的關係。除了原生頁面的解析外,還能夠根據URL跳轉H5頁面。
在服務方組件中都對外提供一個PublicHeader
,在PublicHeader
中聲明當前組件所提供的全部功能,這樣其餘組件想知道當前組件有什麼功能,直接看PublicHeader
便可。每個block
都對應着一個URL
,調用方能夠經過URL
對block
發起調用。
#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
來集成,將全部組件當作二方庫集成到項目中。詳細的集成技術點在下面「標準組件化架構設計」章節中會講到。
下面代碼模擬對詳情頁的註冊、調用,在調用過程當中傳遞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
和參數,Android
和iOS
都使用這一套URL
,能夠保持統一性。
在項目中存在不少公共部分的東西,例如封裝的網絡請求、緩存、數據處理等功能,以及項目中所用到的資源文件。蘑菇街將這些部分也當作組件,劃分爲基礎組件,位於業務組件下層。全部業務組件都使用同一套基礎組件,也能夠保證公共部分的統一性。
爲了解決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
當作值存儲。經過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];
蘑菇街是MGJRouter
和Protocol
混用的方式,兩種實現的調用方式不一樣,但大致調用邏輯和實現思路相似。在MGJRouter
不能知足需求或調用不方便時,就能夠經過Protocol
的方式調用。
MGJRouter
對服務方組件進行註冊。每一個URL
對應一個block
的實現,block
中的代碼就是組件對外提供的服務,調用方能夠經過URL
調用這個服務。MGJRouter
調用openURL:
方法,並將被調用代碼對應的URL
傳入,MGJRouter
會根據URL
查找對應的block
實現,從而調用組件的代碼進行通訊。block
時,block
有一個字典用來傳遞參數。這樣的優點就是參數類型和數量理論上是不受限制的,可是須要不少硬編碼的key
名在項目中。蘑菇街組件化方案有兩種,Protocol
和MGJRouter
的方式,但都須要進行register
操做。Protocol
註冊的是Class
,MGJRouter
註冊的是Block
,註冊表是一個NSMutableDictionary
類型的字典,而字典的擁有者又是一個單例對象,這樣會形成內存的常駐。
下面是對兩種實現方式內存消耗的分析:
MGJRouter
方案可能致使的內存問題,因爲block
會對代碼塊內部對象進行持有,若是使用不當很容易形成內存泄漏的問題。block
自身實際上不會形成很大的內存泄漏,主要是內部引用的變量,因此在使用時就須要注意強引用的問題,並適當使用weak
修飾對應的變量。以及在適當的時候,釋放對應的變量。block
代碼塊內部儘可能不要直接建立對象,應該經過方法調用中轉一下。block
內存常駐方式差很少。只是將存儲的block
對象換成Class
對象。這其實是存儲的類對象,類對象原本就是單例模式,因此不會形成多餘內存佔用。casatwy組件化方案能夠處理兩種方式的調用,遠程調用和本地調用,對於兩個不一樣的調用方式分別對應兩個接口。
AppDelegate
代理方法傳遞到當前應用後,調用遠程接口並在內部作一些處理,處理完成後會在遠程接口內部調用本地接口,以實現本地調用爲遠程調用服務。performTarget:action:params:
方法負責,但調用方通常不直接調用performTarget:
方法。CTMediator
會對外提供明確參數和方法名的方法,在方法內部調用performTarget:
方法和參數的轉換。
casatwy是經過CTMediator
類實現組件化的,在此類中對外提供明確參數類型的接口,接口內部經過performTarget
方法調用服務方組件的Target
、Action
。因爲CTMediator
類的調用是經過runtime
主動發現服務的,因此服務方對此類是徹底解耦的。
但若是CTMediator
類對外提供的方法都放在此類中,將會對CTMediator
形成極大的負擔和代碼量。解決方法就是對每一個服務方組件建立一個CTMediator
的Category
,並將對服務方的performTarget
調用放在對應的Category
中,這些Category
都屬於CTMediator
中間件,從而實現了感官上的接口分離。
對於服務方的組件來講,每一個組件都提供一個或多個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
中定義的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
的方式調用,這個過程就叫作發現服務。
在Target_A
中對傳遞的參數作了處理,以及內部的業務邏輯實現。方法是發生在ModuleA
內部的,這樣就能夠保證組件內部的業務不受外部影響,對內部業務沒有侵入性。
- (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
,能夠區分各個組件的Target
。被調用的Action
命名爲Action_nativeFetchDetailViewController:
,能夠區分組件內的方法與對外提供的方法。
casatwy將類和方法的命名,都統一按照其功能作區分當作前綴,這樣很好的將組件相關和組件內部代碼進行了劃分。
從我調研和使用的結果來講,並不推薦使用Protocol
方案。首先Protocol
方案的代碼量就比MGJRouter
方案的要多,調用和註冊代碼量很大,調用起來並非很方便。
本質上來講Protocol
方案是經過類對象實例一個變量,並調用變量的方法,並無真正意義上的改變組件之間的交互方案,但MGJRouter
的方案卻經過URL Router
的方式改變和統一了組件間調用方式。
而且Protocol
沒有對Remote Router
的支持,不能直接處理來自Push
的調用,在靈活性上就不如MGJRouter
的方案。
我並不推薦CTMediator
方案,這套方案其實是一套很臃腫的方案。雖然爲CTMediator
提供了不少Category
,但實際上組件間的調用邏輯都耦合在了中間件中。一樣,和Protocol
方案存在一個相同的問題,就是調用代碼量很大,使用起來並不方便。
在CTMediator
方案中存在不少硬編碼的問題,例如target
、action
以及參數名都是硬編碼在中間件中的,這種調用方式並不靈活直接。
但casatwy提出了去Model
化的想法,我以爲這在組件化中傳參來講,是很是靈活的,這點我比較認同。相對於MGJRouter
的話,也採用了去Model
化的傳參方式,而不是直接傳遞模型對象。組件化傳參並不適用傳模型對象,但組件內部仍是可使用Model
的。
MGJRouter
方案是一套很是輕量級的方案,其中間件代碼總共也就兩百行之內,很是簡潔。在調用時直接經過URL
調用,調用起來很簡單,我推薦使用這套方案做爲組件化架構的中間件。
MGJRouter
最強大的一點在於,統一了遠程調用和本地調用。這就使得能夠經過Push
的方式,進行任何容許的組件間調用,對項目運營是有很大幫助的。
這三套方案都實現了組件間的解耦,MGJRouter
和Protocol
都是調用方對中間件的耦合,CTMediator
是中間件對組件的耦合,都是單向耦合。
在三套方案中,服務方組件都對外提供一個PublicHeader
或Target
,在文件中統必定義對外提供的服務,組件間通訊的實現代碼大多數都在裏面。
但三套實現方案實現方式並不一樣,蘑菇街的兩套方案都須要註冊操做,不管是Block
仍是Protocol
都須要註冊後才能夠提供服務。而casatwy的方案則不須要,直接經過runtime
調用。
在上面文章中提到了casatwy方案的CTMediator
,蘑菇街方案的MGJRouter
和ModuleManager
,以後將統稱爲中間件,下面讓咱們設計一套組件化架構。
組件化架構中,須要一個主工程,主工程負責集成全部組件。每一個組件都是一個單獨的工程,建立不一樣的git
私有倉庫來管理,每一個組件都有對應的開發人員負責開發。開發人員只須要關注與其相關組件的代碼,不用考慮其餘組件,這樣來新人也好上手。
組件的劃分須要注意組件粒度,粒度根據業務可大可小。組件劃分能夠將每一個業務模塊都劃分爲組件,對於網絡、數據庫等基礎模塊,也應該劃分到組件中。項目中會用到不少資源文件、配置文件等,也應該劃分到對應的組件中,避免重複的資源文件。項目實現徹底的組件化。
每一個組件都須要對外提供調用,在對外公開的類或組件內部,註冊對應的URL
。組件處理中間件調用的代碼應該對其餘代碼無侵入,只負責對傳遞過來的數據進行解析和組件內調用的功能。
每一個組件都是一個單獨的工程,在組件開發完成後上傳到git
倉庫。主工程經過Cocoapods
集成各個組件,集成和更新組件時只須要pod update
便可。這樣就是把每一個組件當作第三方來管理,管理起來很是方便。
Cocoapods
能夠控制每一個組件的版本,例如在主項目中回滾某個組件到特定版本,就能夠經過修改podfile
文件實現。選擇Cocoapods
主要由於其自己功能很強大,能夠很方便的集成整個項目,也有利於代碼的複用。經過這種集成方式,能夠很好的避免在傳統項目中代碼衝突的問題。
對於組件化架構的集成方式,我在看完bang的博客後專門請教了一下bang。根據在微博上和bang的聊天以及其餘博客中的學習,在主項目中集成組件主要分爲兩種方式——源碼和framework
,但都是經過CocoaPods
來集成。
不管是用CocoaPods
管理源碼,仍是直接管理framework
,集成方式都是同樣的,都是直接進行pod update
等CocoaPods
操做。
這兩種組件集成方案,實踐中也是各有利弊。直接在主工程中集成代碼文件,能夠看到其內部實現源碼,方便在主工程中進行調試。集成framework
的方式,能夠加快編譯速度,並且對每一個組件的代碼有很好的保密性。若是公司對代碼安全比較看重,能夠考慮framework
的形式。
例如手機QQ或者支付寶這樣的大型程序,通常都會採起framework
的形式。並且通常這樣的大公司,都會有本身的組件庫,這個組件庫每每能夠表明一個大的功能或業務組件,直接添加項目中就可使用。關於組件化庫在後面講淘寶組件化架構的時候會提到。
對於項目中圖片的集成,能夠把圖片當作一個單獨的組件,組件中只存在圖片文件,沒有任何代碼。圖片可使用Bundle
和image assets
進行管理,若是是Bundle
就針對不一樣業務模塊創建不一樣的Bundle
,若是是image assets
,就按照不一樣的模塊分類創建不一樣的assets
,將全部資源放在同一個組件內。
Bundle
和image assets
二者相比,我仍是更推薦用assets
的方式,由於assets
自身提供不少功能(例如設置圖片拉伸範圍),並且在打包以後圖片會被打包在.cer
文件中,不會被看到。(如今也能夠經過工具對.cer
文件進行解析,獲取裏面的圖片)
使用Cocoapods
,全部的資源文件都放置在一個podspec
中,主工程能夠直接引用這個podspec
,假設此podspec
名爲:Assets
,而這個Assets
的podspec
裏面配置信息能夠寫爲:
s.resources = "Assets/Assets.xcassets/ ** / *.{png}"
主工程則直接在podfile
文件中加入:
pod 'Assets', :path => '../MainProject/Assets'(這種寫法是訪問本地的,能夠換成git)
這樣便可在主工程直接訪問到Assets
中的資源文件(不侷限圖片,sqlite
、js
、html
亦可,在s.resources
設置好配置信息便可)了。
在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
內部作參數截取便可。
使用組件化架構開發,組件間的通訊都是有成本的。因此儘可能將業務封裝在組件內部,對外只提供簡單的接口。即「高內聚、低耦合」原則。
把握好組件劃分粒度的細化程度,太細則項目過於分散,太大則項目組件臃腫。可是項目都是從小到大的一個發展過程,因此不斷進行重構是掌握這個組件的細化程度最好的方式。
若是經過framework
等二進制形式,將組件集成到主項目中,須要注意預編譯指令的使用。由於預編譯指令在打包framework
的時候,就已經在組件二進制代碼中打包好,到主項目中的時候預編譯指令其實已經再也不起做用了,而是已經在打包時按照預編譯指令編碼爲固定二進制。
對於項目架構來講,必定要創建於業務之上來設計架構。不一樣的項目業務不一樣,組件化方案的設計也會不一樣,應該設計最適合公司業務的架構。
我公司項目是一個地圖導航應用,業務層之下的核心模塊和基礎模塊佔比較大,涉及到地圖SDK、算路、語音等模塊。且基礎模塊相對比較獨立,對外提供了不少調用接口。由此能夠看出,公司項目是一個重邏輯的項目,不像電商等App
偏展現。
項目總體的架構設計是:層級架構+組件化架構,對於具體的實現細節會在下面詳細講解。採起這種結構混合的方式進行總體架構,對於組件的管理和層級劃分比較有利,符合公司業務需求。
在設計架構時,咱們將整個項目都拆分爲組件,組件化程度至關高。用到哪一個組件就在工程中經過Podfile
進行集成,並經過URLRouter
統一全部組件間的通訊。
組件化架構是項目的總體框架,而對於框架中每一個業務模塊的實現,能夠是任意方式的架構,MVVM
、MVC
、MVCS
等都是能夠的,只要經過MGJRouter
將組件間的通訊方式統一便可。
組件化架構在物理結構上來講是不分層次的,只有組件與組件之間的劃分關係。可是在組件化架構的基礎上,應該根據項目和業務設計本身的層次架構,這套層次架構能夠用來區分組件所處的層次及職責,因此咱們設計了層級架構+組件化架構的總體架構。
我公司項目最開始設計的是三層架構:業務層 -> 核心層 (high
+ low
) -> 基礎層,其中核心層又分爲high
和low
兩部分。可是這種架構會形成核心層太重,基礎層太輕的問題,這種並不適合組件化架構。
在三層架構中會發現,low
層並無耦合業務邏輯,在同層級中是比較獨立的,職責較爲單一和基礎。咱們對low
層下沉到基礎層中,並和基礎層進行合併。因此架構被從新分爲三層架構:業務層 -> 核心層 -> 基礎層。以前基礎層大可能是資源文件和配置文件,在項目中存在感並不高。
在分層架構中,須要注意只能上層對下層依賴,下層對上層不能有依賴,下層中不要包含上層業務邏輯。對於項目中存在的公共資源和代碼,應該將其下沉到下層中。
在三層架構中,業務層負責處理上層業務,將不一樣業務劃分到相應組件中,例如IM
組件、導航組件、用戶組件等。業務層的組件間關係比較複雜,會涉及到組件間業務的通訊,以及業務層組件對下層組件的引用。
核心層位於業務層下方,爲業務層提供業務支持,如網絡、語音識別等組件應該劃分到核心層。核心層應該儘可能減小組件間的依賴,將依賴降到最小。核心層有時相互之間也須要支持,例如經緯度組件須要網絡組件提供網絡請求的支持,這種是不可避免的。
其餘比較基礎的模塊,都放在基礎層當作基礎組件。例如AFN
、地圖SDK
、加密算法等,這些組件都比較獨立且不摻雜任何業務邏輯,職責更加單一,相對於核心層更底層。能夠包含第三方庫、資源文件、配置文件、基礎庫等幾大類,基礎層組件相互之間不該該產生任何依賴。
在設計各個組件時,應該遵循「高內聚,低耦合」的設計規範,組件的調用應該簡單且直接,減小調用方的其餘處理。 對於核心層和基礎層的劃分,能夠以是否涉及業務、是否涉及同級組件間通訊、是否常常改動爲參照點。若是符合這幾點則放在覈心層,若是不符合則放在基礎層。
新建一個項目後,首先將配置文件、URLRouter
、App
容器等集成到主工程中,作一些基礎的項目配置,隨後集成須要的組件便可。項目被總體拆分爲組件化架構後,應用對全部組件的集成方式都是同樣的,經過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
和傳參名以及回調參數。
組件化架構須要注意路由層的安全問題。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
頁面,若是能經過點擊H5
頁面調起原生頁面,這樣的話Native
和H5
的融合會更好。因此咱們設計了一套H5
和Native
交互的方案,這套方案可使用URLRouter
的方式調起原生頁面,實現方式也很簡單,而且這套方案和H5
本來的跳轉邏輯並不衝突。
經過iOS
自帶UIWebView
建立一個H5
頁面後,H5
能夠經過調用下面的JS
函數和Native
通訊。調用時能夠傳入新的URL
,這個URL
能夠設置爲URLRouter
的URL
。
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
頁面喚起原生頁面。
在應用啓動過程當中,一般會作一些初始化操做。有些初始化操做是運行程序所須要的,例如崩潰統計、創建服務器的長鏈接等。或有的組件會對初始化操做有依賴關係,例如網絡組件依賴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
項目中存在不少的模型定義,那組件化後這些模型應該定義在哪呢?
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
客戶端初期是單工程的普通項目,但隨着業務的飛速發展,現有架構並不能承載愈來愈多的業務需求,致使代碼間耦合很嚴重。後期開發團隊對其不斷進行重構,將項目重構爲組件化架構,淘寶iOS
和Android
兩個平臺,除了某個平臺特有的一些特性或某些方案不便實施以外,大致架構都是差很少的。
發展歷程
MVC
架構進行開發。隨着業務不斷的增長,致使項目很是臃腫、耦合嚴重。淘寶開始實行插件化架構,將每一個業務模塊劃分爲一個子工程,將組件以framework
二方庫的形式集成到主工程。但這種方式並無作到真正的拆分,仍是在一個工程中使用git
進行merge
,這樣還會形成合並衝突、很差回退等問題。
framework
。主工程經過podfile
集成全部組件的framework
,實現業務之間真正的隔離,經過CocoaPods
實現組件化架構。淘寶是使用git
來作源碼管理的,在插件化架構時須要儘量避免merge
操做,不然在大團隊中協做成本是很大的。而使用CocoaPods
進行組件化開發,則避免了這個問題。
在CocoaPods
中能夠經過podfile
很好的配置各個組件,包括組件的增長和刪除,以及控制某個組件的版本。使用CocoaPods
的緣由,很大程度是爲了解決大型項目中,代碼管理工具merge
代碼致使的衝突。而且能夠經過配置podfile
文件,輕鬆配置項目。
每一個組件工程有兩個target
,一個負責編譯當前組件和運行調試,另外一個負責打包framework
。先在組件工程作測試,測試完成後再集成到主工程中集成測試。
每一個組件都是一個獨立app
,能夠獨立開發、測試,使得業務組件更加獨立,全部組件能夠並行開發。下層爲上層提供能知足需求的底層庫,保證上層業務層能夠正常開發,並將底層庫封裝成framework
集成到主工程中。
使用CocoaPods
進行組件集成的好處在於,在集成測試本身組件時,能夠直接在本地主工程中,經過podfile
使用當前組件源碼,能夠直接進行集成測試,不須要提交到服務器倉庫。
淘寶架構的核心思想是一切皆組件,將工程中全部代碼都抽象爲組件。
淘寶架構主要分爲四層,最上層是組件Bundle
(業務組件),依次往下是容器(核心層),中間件Bundle
(功能封裝),基礎庫Bundle
(底層庫)。容器層爲整個架構的核心,負責組件間的調度和消息派發。
總線設計:URL
路由+服務+消息。統一全部組件的通訊標準,各個業務間經過總線進行通訊。
經過URL
總線對三端進行了統一,一個URL
能夠調起iOS
、Android
、前端三個平臺,產品運營和服務器只須要下發一套URL
便可調用對應的組件。
URL
路由能夠發起請求也能夠接受返回值,和MGJRouter
差很少。URL
路由請求能夠被解析就直接拿來使用,若是不能被解析就跳轉H5
頁面。這樣就完成了一個對不存在組件調用的兼容,使用戶手中比較老的版本依然能夠顯示新的組件。
服務提供一些公共服務,由服務方組件負責實現,經過Protocol
進行調用。
應用經過消息總線進行事件的中心分發,相似於iOS
的通知機制。例如客戶端先後臺切換,則能夠經過消息總線分發到接收消息的組件。由於經過URLRouter
只是一對一的進行消息派發和調度,若是屢次註冊同一個URL
,則會被覆蓋掉。
在組件化架構的基礎上,淘寶提出Bundle App
的概念,能夠經過已有組件,進行簡單配置後就能夠組成一個新的app
出來。解決了多個應用業務複用的問題,防止重複開發同一業務或功能。
Bundle
即App
,容器即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😁。
因爲簡書排版並非很好,因此作了一個PDF
版的《組件化架構漫談》,放在我Github
上了。PDF
上有文章目錄,方便閱讀,下面是地址。
若是你以爲不錯,請把PDF幫忙轉到其餘羣裏,或者你的朋友,讓更多的人瞭解組件化架構,衷心感謝! 😁
Github地址 : https://github.com/DeveloperErenLiu/ComponentArchitectureBook