什么是組件化
在看了很多其他人的方案之后,首先對(duì)組件化思想上有一個(gè)小分歧。我認(rèn)為很多人對(duì)于iOS中組件化的理解其實(shí)是有誤區(qū)的。我剛工作的第一年就是在做Flex開發(fā),其中就有很多組件化的思想,加上最近在用Vue做web項(xiàng)目之后,更為意識(shí)到大家在iOS開發(fā)上說的組件化有點(diǎn)不合適。
首先我認(rèn)為組件是一個(gè)相對(duì)比較小的功能模塊,不需要與外界有太多的通信,更不能依賴其他第三方,這一點(diǎn)尤為重要。比如說幾乎所有iOS開發(fā)者都知道的MJReflesh,比如我很早之前開源的?MTMessageKeyBoard?這些幾乎不依賴業(yè)務(wù),并且提供了良好的調(diào)用接口和使用體驗(yàn)的才能稱為組件。而看了很多方案,大部分都是在講app里面的業(yè)務(wù)功能組件之間的通信和解耦,其實(shí)我更愿意將這些東西稱為“模塊”。那如何區(qū)分這兩種呢,我覺得這句話比較好理解:
核心業(yè)務(wù)模塊化,通用功能組件化
打比方說你的app是一個(gè)電商項(xiàng)目,那么你的產(chǎn)品詳情頁、列表頁、購物車、搜索等頁面肯定就是調(diào)用頻次非常高的VC了,這些界面之間跳轉(zhuǎn)都會(huì)非常頻繁。這就造成了互相依賴并且高度耦合。如下圖所示。

看起來是不是跟個(gè)麻花一樣?而怎么樣才能解決這個(gè)問題呢? 等下會(huì)講。
像商品詳情頁這些通常外部調(diào)入只需要傳入一個(gè)productID就可以,而且高度依賴自己的業(yè)務(wù)功能的模塊就可以將這些當(dāng)成一個(gè)模塊維護(hù)。后面需要修改里面的活動(dòng)的顯示、業(yè)務(wù)的增刪都可以單獨(dú)在詳情模塊里面改動(dòng)而不需要改動(dòng)別的代碼。
而對(duì)于組件,比方說我上面提到的IM類型的app中用到的聊天鍵盤,或者集成支付寶、微信等支付功能的支付插件。這些可以在多個(gè)不同的項(xiàng)目小組內(nèi)部共享。甚至可以開源到社區(qū)中供所有開發(fā)者使用的小插件,用組件來形容更貼切。在flex、Vue、angular等前端開發(fā)中體現(xiàn)尤為突出。
所以接下來我所要講的組件化實(shí)際上更貼切的說法是模塊化。
客戶端在公司業(yè)務(wù)發(fā)展的過程中體積越來越龐大,其中堆疊了大量的業(yè)務(wù)邏輯代碼,不同業(yè)務(wù)模塊的代碼相互調(diào)用,相互嵌套,代碼之間的耦合性越來越高,調(diào)用邏輯會(huì)越來越混亂。當(dāng)某個(gè)模塊需要升級(jí)的時(shí)候,改動(dòng)代碼的時(shí)候往往會(huì)有牽一發(fā)而動(dòng)全身的感覺。特別是如果工程最初設(shè)計(jì)的時(shí)候沒有考慮的接口的封裝,而將大量的業(yè)務(wù)代碼與功能模塊代碼混在一起時(shí),將來的升級(jí)就需要對(duì)代碼進(jìn)行大量修改及調(diào)整,帶來的工作量是非常巨大的。這就需要設(shè)計(jì)一套符合要求的組件之間通信的中間件。模塊化可以將代碼的功能邏輯盡量封裝在一起,對(duì)外只提供接口,業(yè)務(wù)邏輯代碼與功能模塊通過接口進(jìn)行弱耦合。
封裝模塊的工作只要你對(duì)面向?qū)ο笏枷胗兴斫?,?shí)現(xiàn)起來應(yīng)該不難,確保寫好調(diào)用接口就行,這里不再贅述。而模塊化最重要的就是各個(gè)模塊之間的通信。比如在商品搜索列表頁面,需要查看購物車功能和查看商品詳情功能,購物車的商品列表也能點(diǎn)擊商品到商品詳情頁。等等這些界面之間都會(huì)相互調(diào)用,相互依賴。通常我們會(huì)怎么實(shí)現(xiàn)呢?比如這樣:
#import "ProductDetailViewController.h"
#import "CartViewController.h"
@implementation ProductListViewController
- (void)gotoDetail {
ProductDetailViewController *detailVC = [[ProductDetailViewController alloc] initWithProId:self.proId];
[self.navigationController pushViewController:detailVC animated:YES];
}
- (void)gotoCart {
CartViewController *cartVC = [[CartViewController alloc] init];
[self.navigationController pushViewController: cartVC animated:YES];
}
@end
相信這樣的代碼大家都不陌生,基本都是這樣做。而且這樣寫也并沒有問題。但是,項(xiàng)目一旦大起來問題就來了。 各個(gè)模塊只要有相互調(diào)用的情況,都會(huì)相互產(chǎn)生依賴。每次跳轉(zhuǎn)都需要import對(duì)應(yīng)的控制器,重寫一次代碼。如果某個(gè)地方做了一點(diǎn)點(diǎn)需求改動(dòng),比如商品詳情頁需要多傳入一個(gè)參數(shù),這個(gè)時(shí)候就要找到各個(gè)調(diào)用的地方逐一修改。這顯然不是高效的辦法。
于是很簡單的就想到了一個(gè)方法,提供一個(gè)中間層:Router。在router里面定義好每次跳轉(zhuǎn)的方法,然后在需要用的界面調(diào)用router函數(shù),傳入對(duì)應(yīng)的參數(shù)。比如這樣:
//Router.m
#import "ProductDetailViewController.h"
#import "CartViewController.h"
@implementation Router
+ (UIViewController *) getDetailWithParam:(NSString *) param {
ProductDetailViewController *detailVC = [[ProductDetailViewController alloc] initWithProId:self.proId];
return detailVC;
}
+ (UIViewController *) getCart {
? CartViewController *cartVC = [[CartViewController alloc] init];
? return cartVC;
}
@end
其他界面中這樣使用
#import "Router.m"
UIViewController * detailVC = [[Router instance] jumpToDetailWithParam:param];
[self.navigationController pushViewController: detailVC];
但是這樣寫的話也有一個(gè)問題,每個(gè)vc都會(huì)依賴Router,而Router里面會(huì)依賴所有的VC。那如何打破這層循環(huán)引用呢?OC里有個(gè)法寶可以用到:runtime。
-(UIViewController *)getViewController:(NSString *)stringVCName
{
? Class class = NSClassFromString(stringVCName);
? ? UIViewController *controller = [[class alloc] init];
? ? if(controller == nil){
? ? ? ? NSLog(@"未找到此類:%@",stringVCName);
? ? ? ? controller = [[RouterError sharedInstance] getErrorController];
? ? }
? ? return controller;
}
這樣上面的圖就是這樣的:

這樣Router里面不需要import任何vc了,代碼也就數(shù)十行而已,看起來非常的簡便。而且做了異常處理,如果找不到此類,會(huì)返回預(yù)先設(shè)置的錯(cuò)誤界面。是不是有點(diǎn)類似于web開發(fā)中的404界面呢?
? UIViewController *controller = [[Router sharedInstance] getViewController:@"ProductDetailViewController"];
? [self.navigationController pushViewController:controller];? ? ? ? ?
很多人肯定都發(fā)現(xiàn)了,這樣寫的話如何傳參數(shù)呢。比如商品詳情頁至少要傳一個(gè)productID吧。別急,我們可以將上面的方法稍微處理,傳入一個(gè)dict做了參數(shù)。
-(UIViewController *)getViewController:(NSString *)stringVCName
{
? ? Class class = NSClassFromString(stringVCName);
? ? UIViewController *controller = [[class alloc] init];
? ? return controller;
}
-(UIViewController *)getViewController:(NSString *)stringVCName withParam:(NSDictionary *)paramdic
{
? ? UIViewController *controller = [self getViewController:stringVCName];
? ? if(controller != nil){
? ? ? ? controller = [self controller:controller withParam:paramdic andVCname:stringVCName];
? ? }else{
? ? ? ? NSLog(@"未找到此類:%@",stringVCName);
? ? ? ? //EXCEPTION? Push a Normal Error VC
? ? ? ? controller = [[RouterError sharedInstance] getErrorController];
? ? }
? ? return controller;
}
/**
此方法用來初始化參數(shù)(控制器初始化方法默認(rèn)為 initViewControllerParam。初始化方法你可以自定義,前提是VC必須實(shí)現(xiàn)它。要想靈活一點(diǎn),也可以加一個(gè)參數(shù)actionName,當(dāng)做參數(shù)傳入。不過這樣你就需要修改此方法了)。
@param controller 獲取到的實(shí)例VC
@param paramdic 實(shí)例化參數(shù)
@param vcName 控制器名字
@return 初始化之后的VC
*/
-(UIViewController *)controller:(UIViewController *)controller withParam:(NSDictionary *)paramdic andVCname:(NSString *)vcName {
? ? SEL selector = NSSelectorFromString(@"initViewControllerParam:");
? ? if(![controller respondsToSelector:selector]){? //如果沒定義初始化參數(shù)方法,直接返回,沒必要在往下做設(shè)置參數(shù)的方法
? ? ? ? NSLog(@"目標(biāo)類:%@未定義:%@方法",controller,@"initViewControllerParam:");
? ? ? ? return controller;
? ? }
? ? //在初始化參數(shù)里面添加一個(gè)key信息,方便控制器中查驗(yàn)路由信息
? ? if(paramdic == nil){
? ? ? ? paramdic = [[NSMutableDictionary alloc] init];
? ? ? ? [paramdic setValue:vcName forKey:@"URLKEY"];
? ? ? ? SuppressPerformSelectorLeakWarning([controller performSelector:selector withObject:paramdic]);
? ? }else{
? ? ? ? [paramdic setValue:vcName forKey:@"URLKEY"];
? ? }
? ? SuppressPerformSelectorLeakWarning( [controller performSelector:selector withObject:paramdic]);
? ? return controller;
}
我們默認(rèn)在業(yè)務(wù)控制器里面有個(gè)initViewControllerParam方法,然后再router里面可以用respondsToSelector手動(dòng)觸發(fā)這個(gè)方法,傳入?yún)?shù)paramdict。當(dāng)然如果你想要更加靈活一點(diǎn),那就將initViewControllerParam初始化方法當(dāng)做一個(gè)actionName參數(shù)傳到router里面。類似于這樣:
-(UIViewController *)controller:(UIViewController *)controller withParam:(NSDictionary *)paramdic andVCname:(NSString *)vcName actionName:(NSString *)actionName{
? ? SEL selector = NSSelectorFromString(actionName);
? ? ....后面就是一樣的代碼了
到這里基本上模塊化就可以實(shí)現(xiàn)了。基本上通過不超過100行的代碼解決了各個(gè)復(fù)雜業(yè)務(wù)模塊之間的通信和高度解耦。
模塊化的實(shí)現(xiàn)方法在iOS開發(fā)中算是比較好實(shí)現(xiàn)的,主要是OC本身就是一門動(dòng)態(tài)的語言。對(duì)象類型是加上是在運(yùn)行時(shí)中確定的,而調(diào)用方法在oc中是以發(fā)消息的形式實(shí)現(xiàn)。這就增加了很多可以操作的可能性。這種方法在大部分的app中都能很好的應(yīng)用,并且解決大部分的業(yè)務(wù)需求。
而博客開始之前提到的組件化,其實(shí)一般在小中型的項(xiàng)目里面用到的可能性不大,除非業(yè)務(wù)真的非常復(fù)雜和龐大。有至少10個(gè)或者以上的開發(fā)者才會(huì)遇到將部分通用的功能抽離出來做成組件。iOS中組件化目前最合適也是最成熟的就是CocoaPods私有庫了,直接托管到內(nèi)部git服務(wù)器上使用,或者開源到gitHub上讓全球的開發(fā)者使用你的插件。
類似于中間件的模塊化方案其實(shí)之前在項(xiàng)目中我有實(shí)踐,確實(shí)能解決一部分的開發(fā)問題和效率。但是在和很多其他開發(fā)者交流中發(fā)現(xiàn),其實(shí)業(yè)務(wù)的復(fù)雜度很難支持你做好真正的模塊化。經(jīng)常都是良好的開端,最后由于項(xiàng)目推進(jìn)中帶來的需求更改,促使你不斷修改這個(gè)所謂的中間件。到最后還是揉成一坨,帶來更多新的問題。所以,在實(shí)踐中大家仁者見仁智者見智吧,但是本文中所提到的方案還是有必要了解和掌握的。
畢竟胸有兵法,不怕干仗嘛。
~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~
作者:MrTung
來源:簡書
鏈接:https://github.com/MrTung/MTRouter
著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處。
~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~