iOS組件化曾今在業(yè)界是多么的火熱的話題震贵,現(xiàn)在在少有人再次提及這個(gè)的話題。網(wǎng)上也很多關(guān)于組件化的文章和思想水评,最經(jīng)典的要是casa大神和蘑菇街關(guān)于組件化的論戰(zhàn)猩系。想想曾今看到這些文章的時(shí)候,覺得組件化是多么優(yōu)秀的思想中燥,覺得他們說的都有道理寇甸,而casa大神應(yīng)該在很多思想上給了我等碼農(nóng)很多靈感。而兩位大神架構(gòu)師級(jí)別的論劍是否讓你真正理解到組件化的重要性疗涉。是否讓你在內(nèi)心深處產(chǎn)生共鳴拿霉,最 近看到一個(gè)項(xiàng)目讓我對(duì)組件化多了些思考。
一咱扣、為什么要組件化绽淘,組件化到底有什么好處?
為什么要組件化闹伪,在看過很多優(yōu)秀的文章后沪铭,你一定會(huì)問這個(gè)問題,組件化能給我們帶來多大的好處偏瓤?作為一個(gè)小公司而言杀怠,涉及組件化的機(jī)會(huì)很少,沒有大廠的工作經(jīng)驗(yàn)硼补,也很難將組件化理解的很透徹驮肉⊙螅可能以為我們的業(yè)務(wù)模塊還不夠多已骇,或者說,我們沒有理解到他的好處票编,其實(shí)組件化最大的好處就是褪储,每個(gè)組件,每個(gè)模塊都可能單獨(dú)成一個(gè)app慧域,具有自己的生命周期鲤竹。這樣就可以分割成不同的業(yè)務(wù)組模塊去處理,之前聽說京東昔榴,有團(tuán)隊(duì)專門負(fù)責(zé)消息模塊辛藻,有團(tuán)隊(duì)專門負(fù)責(zé)廣告模塊,有團(tuán)隊(duì)專門負(fù)責(zé)發(fā)現(xiàn)模塊互订,這是你就會(huì)發(fā)現(xiàn)如果沒有很好的組件化思想吱肌,這樣的多團(tuán)隊(duì)合作就非常的困難,已經(jīng)很難維護(hù)好這個(gè)項(xiàng)目的開發(fā)迭代仰禽。說了這么多氮墨,到底組件化是什么樣子的呢纺蛆?那我跟著我的腳步,學(xué)習(xí)分析规揪,探討下桥氏。
二、組件化的核心思想
組件化的話的核心思想猛铅,也是我們進(jìn)行組件化的基礎(chǔ)框架字支,就是通過怎么樣的方式實(shí)現(xiàn)組件化,或者如何從架構(gòu)層奕坟,業(yè)務(wù)層多個(gè)層次實(shí)現(xiàn)架構(gòu)呢祥款。要想實(shí)現(xiàn)組件化,其實(shí)就是建立一個(gè)中間轉(zhuǎn)換的工具月杉。你也可以理解為路由刃跛,通過路由的思想實(shí)現(xiàn)跨業(yè)務(wù)的數(shù)據(jù)溝通,從而一定程度上的降低各層數(shù)據(jù)的耦合苛萎。減少各個(gè)業(yè)務(wù)層等層級(jí)的import發(fā)生的耦合桨昙。
三、目前實(shí)現(xiàn)的組件化的方式
目前實(shí)現(xiàn)一般有下面三種思想:
1.Procotol方案
2.URL路由方案
3.target-action方案
Procotol協(xié)議注冊方案
關(guān)于procotol協(xié)議注冊方案看人用的比較少腌歉,也很少看到有人分享蛙酪,我也是在這個(gè)項(xiàng)目中看到,就研究了一下翘盖。通過JJProtocolManager 作為中間轉(zhuǎn)化桂塞。
+ (void)registerModuleProvider:(id)provider forProtocol:(Protocol*)protocol;
+ (id)moduleProviderForProtocol:(Protocol *)protocol;
所有組件對(duì)外提供的procotol和組件提供的服務(wù)由中間件統(tǒng)一管理,每個(gè)組件提供的procotol和服務(wù)是一一對(duì)應(yīng)的馍驯。
例如:
在JJLoginProvider中:load方法會(huì)應(yīng)用啟動(dòng)的時(shí)候調(diào)用阁危,就會(huì)在JJProtocolManager進(jìn)行注冊。JJLoginProvider遵守了JJLoginProvider協(xié)議汰瘫,這樣就可以對(duì)外根據(jù)業(yè)務(wù)需求提供一些方法狂打。
+ (void)load
{
[JJProtocolManager registerModuleProvider:[self new] forProtocol:@protocol(JJLoginProtocol)];
}
- (UIViewController *)viewControllerWithInfo:(id)userInfo needNew:(BOOL)needNew callback:(JJModuleCallbackBlock)callback{
CLoginViewController *vc = [[CLoginViewController alloc] init];
vc.jj_moduleCallbackBlock = callback;
vc.jj_moduleUserInfo = userInfo;
return vc;
}
這樣就可以在需要登錄業(yè)務(wù)模塊的地方,通過JJProtocolManager取出JJLoginProtocol對(duì)應(yīng)的服務(wù)提供者JJLoginProvider混弥,直接獲取趴乡。如下:
id<JJWebviewVCModuleProtocol> provider = [JJProtocolManager moduleProviderForProtocol:@protocol(JJWebviewVCModuleProtocol)];
UIViewController *vc =[provider viewControllerWithInfo:obj needNew:YES callback:^(id info) {
if (callback) {
callback(info);
}
}];
vc.hidesBottomBarWhenPushed = YES;
[self.currentNav pushViewController:vc animated:YES];
URL路由方案
URL路由方案最經(jīng)典的就是蘑菇街的路由組件化,通過url的方式將調(diào)用方法蝗拿,調(diào)用參數(shù)晾捏,已經(jīng)回調(diào)方法封裝到url中,然后在通過對(duì)url的解析獲取到方法名哀托,參數(shù)惦辛,最后通過消息轉(zhuǎn)發(fā)機(jī)制調(diào)用方法。
下面是蘑菇街的路由方式:(這里要是想詳細(xì)了解萤捆,可以到蘑菇街的路由組件化 中具體學(xué)習(xí))
[MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) {
NSNumber *id = routerParameters[@"id"];
// create view controller with id
// push view controller
}];
首頁只需調(diào)用 [MGJRouter openURL:@"mgj://detail?id=404"] 就可以打開相應(yīng)的詳情頁裙品。
這里可以看到俗批,我們通過url短鏈的方式,通過將參數(shù)拼接到url query部分市怎,這樣就可以岁忘,通過這樣解析url中的scheme,host,path,query獲取到調(diào)轉(zhuǎn)什么要的控制器,需要傳什么什么樣的參數(shù)区匠,從而push或者present新頁面干像。
解析scheme,host,path核心代碼
NSString *scheme = [nsUrl scheme];//解析scheme
NSString *module = [nsUrl host];
NSString *action = [[nsUrl path] stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
if (action && [action length] && [action hasPrefix:@"_"]) {
action = [action stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:@""];
}
NSString *query = nil;
NSArray* pathInfo = [nsUrl.absoluteString componentsSeparatedByString:@"?"];
if (pathInfo.count > 1) {
query = [pathInfo objectAtIndex:1];
}
解析query的核心代碼
NSMutableDictionary *parameters = nil;
NSString *parametersString = query;
NSArray *paramStringArr = [parametersString componentsSeparatedByString:@"&"];
if (paramStringArr && [paramStringArr count]>0) {
parameters = [NSMutableDictionary dictionary];
for (NSString* paramString in paramStringArr) {
NSArray *paramArr = [paramString componentsSeparatedByString:@"="];
if (paramArr.count > 1) {
NSString *key = [paramArr objectAtIndex:0];
NSString *value = [paramArr objectAtIndex:1];
parameters[key] = [JJRouter unescapeURIComponent:value];
}
}
}
return parameters;
通過這樣的方式,我們就可以實(shí)現(xiàn)組件化驰弄,但是有時(shí)候我們會(huì)遇到一個(gè)圖片編輯模塊麻汰,不能傳遞UIImage到對(duì)應(yīng)的模塊上去的話,這里我們需要傳個(gè)新的參數(shù)進(jìn)去戚篙,為了解決這個(gè)問題五鲫,這樣其實(shí),可以把參數(shù)直接丟給后面的arg處理
+ (nullable id)openURL:(nonnull NSString *)urlString arg:(nullable id)arg error:( NSError*__nullable *__nullable)error completion:(nullable JJRouterCompletion)completion
舉個(gè)例子:
Action *action = [Action new];
action.type = JJ_WebView;
Params *params = [[Params alloc] init];
// params.pageID = JJ_LOGIN;
action.params = params;
NSDictionary *parms = @{Jump_Key_Action:action, Jump_Key_Param : @{WebUrlString:@"http://www.baidu.com",Name:@"小二"}, Jump_Key_Callback:[JJFunc callback:^(id _Nullable object) {
NSLog(@"%@",object);
}]};
// ActionJump(parms);
[JJRouter openURL:@"router://JJActionService/showWebVC" arg: parms error:nil completion:parms[Jump_Key_Callback]];
}
我看的項(xiàng)目岔擂,這個(gè)就是通過url解析和protocol協(xié)議注冊實(shí)現(xiàn)組件化,只是沒有像蘑菇街那樣注冊支持哪些 URL類型位喂。
target-action方案
target-action方案是在學(xué)習(xí)casa大神,CTMediator 的基礎(chǔ)上進(jìn)行的
casa大神認(rèn)為乱灵,
1.根本無法表達(dá)非常規(guī)對(duì)象塑崖,如果用url組件化的話,遇到像UIImage這樣的參數(shù)痛倚,就需要添加一個(gè)參數(shù)规婆,才能解決
2.URL注冊對(duì)于實(shí)施組件化方案是完全不必要的,且通過URL注冊的方式形成的組件化方案蝉稳,拓展性和可維護(hù)性都會(huì)被打折
3.蘑菇街沒有拆分遠(yuǎn)程調(diào)用和本地間調(diào)用
4.蘑菇街必須要在app啟動(dòng)時(shí)注冊URL響應(yīng)者
//理論上頁面之間的跳轉(zhuǎn)只需 open 一個(gè) URL 即可抒蚜。所以對(duì)于一個(gè)組件來說,只要定義「支持哪些 URL」即可颠区,比如詳情頁削锰,大概可以這么做的
[MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) {
NSNumber *id = routerParameters[@"id"];
// create view controller with id
// push view controller
}];
而casa的組件化主要是基于Mediator模式和Target-Action模式通铲,中間采用了runtime來完成調(diào)用毕莱。這套組件化方案將遠(yuǎn)程應(yīng)用調(diào)用和本地應(yīng)用調(diào)用做了拆分,而且是由本地應(yīng)用調(diào)用為遠(yuǎn)程應(yīng)用調(diào)用提供服務(wù)颅夺,與蘑菇街方案正好相反朋截。
調(diào)用方式:
先說本地應(yīng)用調(diào)用,本地組件A在某處調(diào)用[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]向CTMediator發(fā)起跨組件調(diào)用吧黄,CTMediator根據(jù)獲得的target和action信息部服,通過objective-C的runtime轉(zhuǎn)化生成target實(shí)例以及對(duì)應(yīng)的action選擇子,然后最終調(diào)用到目標(biāo)業(yè)務(wù)提供的邏輯拗慨,完成需求廓八。
在遠(yuǎn)程應(yīng)用調(diào)用中奉芦,遠(yuǎn)程應(yīng)用通過openURL的方式,由iOS系統(tǒng)根據(jù)info.plist里的scheme配置找到可以響應(yīng)URL的應(yīng)用(在當(dāng)前我們討論的上下文中剧蹂,這就是你自己的應(yīng)用)声功,應(yīng)用通過AppDelegate接收到URL之后,調(diào)用CTMediator的openUrl:方法將接收到的URL信息傳入宠叼。當(dāng)然先巴,CTMediator也可以用openUrl:options:的方式順便把隨之而來的option也接收,這取決于你本地業(yè)務(wù)執(zhí)行邏輯時(shí)的充要條件是否包含option數(shù)據(jù)冒冬。傳入U(xiǎn)RL之后伸蚯,CTMediator通過解析URL,將請(qǐng)求路由到對(duì)應(yīng)的target和action简烤,隨后的過程就變成了上面說過的本地應(yīng)用調(diào)用的過程了剂邮,最終完成響應(yīng)。
針對(duì)請(qǐng)求的路由操作很少會(huì)采用本地文件記錄路由表的方式横侦,服務(wù)端經(jīng)常處理這種業(yè)務(wù)抗斤,在服務(wù)端領(lǐng)域基本上都是通過正則表達(dá)式來做路由解析。App中做路由解析可以做得簡單點(diǎn)丈咐,制定URL規(guī)范就也能完成瑞眼,最簡單的方式就是scheme://target/action這種,簡單做個(gè)字符串處理就能把target和action信息從URL中提取出來了棵逊。
舉個(gè)例子:
/**
這里是登錄模塊的target
**/
#import "CTMediator+ModuleLogin.h"
NSString * const kCTMediatorTargetA = @"A";
NSString * const kCTMediatorActionLoginViewController = @"showLoginController";
@implementation CTMediator (ModuleLogin)
- (UIViewController *)push_viewControllerForLogin
{
UIViewController *vc = [self performTarget:kCTMediatorTargetA action:kCTMediatorActionLoginViewController params:nil shouldCacheTarget:NO];
if ([vc isKindOfClass:[UIViewController class]]) {
// view controller 交付出去之后伤疙,可以由外界選擇是push還是present
return vc;
} else {
// 這里處理異常場景,具體如何處理取決于產(chǎn)品
return [[UIViewController alloc] init];
}
}
/**
登錄模塊的action
**/
- (UIViewController *)Action_showLoginController:(NSDictionary *)param
{
JJLoginViewController *vc =[[JJLoginViewController alloc] init];
return vc;
}
看上去辆影,target-action路由方案更加的清晰徒像,不過這個(gè)還是各取所需吧
接下來,target-action的核心代碼就是
/**
if ([target respondsToSelector:action])
判斷target能否響應(yīng)action方法蛙讥,只要能夠就執(zhí)行這段核心代碼锯蛀,
核心代碼的主要功能:
**/
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
//// 創(chuàng)建一個(gè)函數(shù)簽名,這個(gè)簽名可以是任意的次慢,但需要注意旁涤,簽名函數(shù)的參數(shù)數(shù)量要和調(diào)用的一致。
NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
if(methodSig == nil) {
return nil;
}
// 獲取返回類型
const char* retType = [methodSig methodReturnType];
//判斷返回值類型
if (strcmp(retType, @encode(void)) == 0) {
// 通過簽名初始化
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
//如果此消息有參數(shù)需要傳入迫像,那么就需要按照如下方法進(jìn)行參數(shù)設(shè)置劈愚,需要注意的是,atIndex的下標(biāo)必須從2開始闻妓。原因?yàn)椋? 1 兩個(gè)參數(shù)已經(jīng)被target 和selector占用
[invocation setArgument:¶ms atIndex:2];
// 設(shè)置selector
[invocation setSelector:action];
// 設(shè)置target
[invocation setTarget:target];
//消息調(diào)用
[invocation invoke];
return nil;
}
if (strcmp(retType, @encode(NSInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(BOOL)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
BOOL result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(CGFloat)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
CGFloat result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(NSUInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSUInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}
總結(jié):
CTMediator根據(jù)獲得的target和action信息菌羽,通過objective-C的runtime轉(zhuǎn)化生成target實(shí)例以及對(duì)應(yīng)的action選擇子,然后最終調(diào)用到目標(biāo)業(yè)務(wù)提供的邏輯由缆,完成需求注祖。
下面是三種方式的代碼實(shí)現(xiàn)Git的地址:
https://github.com/lumig/JJRouterDemo
彩蛋:
// url 編碼格式
foo://example.com:8042/over/there?name=ferret#nose
\_/ \______________/ \________/\_________/ \__/
| | | | |
scheme authority path query fragment
scheme://host.domain:port/path/filename
scheme - 定義因特網(wǎng)服務(wù)的類型猾蒂。最常見的類型是 http
host - 定義域主機(jī)(http 的默認(rèn)主機(jī)是 www)
domain - 定義因特網(wǎng)域名,比如 w3school.com.cn
:port - 定義主機(jī)上的端口號(hào)(http 的默認(rèn)端口號(hào)是 80)
path - 定義服務(wù)器上的路徑(如果省略是晨,則文檔必須位于網(wǎng)站的根目錄中)婚夫。
filename - 定義文檔/資源的名稱