iOS組件化不只是架構(gòu)師的事
iOS組件化曾今在業(yè)界是多么的火熱的話題,現(xiàn)在在少有人再次提及這個的話題。網(wǎng)上也很多關(guān)于組件化的文章和思想,最經(jīng)典的要是casa大神和蘑菇街關(guān)于組件化的論戰(zhàn)。想想曾經(jīng)看到這些文章的時候,覺得組件化是多么優(yōu)秀的思想,覺得他們說的都有道理,而casa大神應(yīng)該在很多思想上給了我等碼農(nóng)很多靈感。而兩位大神架構(gòu)師級別的論劍是否讓你真正理解到組件化的重要性。是否讓你在內(nèi)心深處產(chǎn)生共鳴,最 近看到一個項(xiàng)目讓我對組件化多了些思考。
一、為什么要組件化,組件化到底有什么好處?
為什么要組件化,在看過很多優(yōu)秀的文章后,你一定會問這個問題,組件化能給我們帶來多大的好處?作為一個小公司而言,涉及組件化的機(jī)會很少,沒有大廠的工作經(jīng)驗(yàn),也很難將組件化理解的很透徹??赡芤詾槲覀兊臉I(yè)務(wù)模塊還不夠多,或者說,我們沒有理解到他的好處,其實(shí)組件化***的好處就是,每個組件,每個模塊都可能單獨(dú)成一個app,具有自己的生命周期。這樣就可以分割成不同的業(yè)務(wù)組模塊去處理,之前聽說京東,有團(tuán)隊(duì)專門負(fù)責(zé)消息模塊,有團(tuán)隊(duì)專門負(fù)責(zé)廣告模塊,有團(tuán)隊(duì)專門負(fù)責(zé)發(fā)現(xiàn)模塊,這是你就會發(fā)現(xiàn)如果沒有很好的組件化思想,這樣的多團(tuán)隊(duì)合作就非常的困難,已經(jīng)很難維護(hù)好這個項(xiàng)目的開發(fā)迭代。說了這么多,到底組件化是什么樣子的呢?那我跟著我的腳步,學(xué)習(xí)分析,探討下。
二、組件化的核心思想
組件化的話的核心思想,也是我們進(jìn)行組件化的基礎(chǔ)框架,就是通過怎么樣的方式實(shí)現(xiàn)組件化,或者如何從架構(gòu)層,業(yè)務(wù)層多個層次實(shí)現(xiàn)架構(gòu)呢。要想實(shí)現(xiàn)組件化,其實(shí)就是建立一個中間轉(zhuǎn)換的工具。你也可以理解為路由,通過路由的思想實(shí)現(xiàn)跨業(yè)務(wù)的數(shù)據(jù)溝通,從而一定程度上的降低各層數(shù)據(jù)的耦合。減少各個業(yè)務(wù)層等層級的import發(fā)生的耦合。
三、目前實(shí)現(xiàn)的組件化的方式
目前實(shí)現(xiàn)一般有下面三種思想:
- Procotol方案
- URL路由方案
- target-action方案
Procotol協(xié)議注冊方案
關(guān)于procotol協(xié)議注冊方案看人用的比較少,也很少看到有人分享,我也是在這個項(xiàng)目中看到,就研究了一下。通過JJProtocolManager 作為中間轉(zhuǎn)化。
- + (void)registerModuleProvider:(id)provider forProtocol:(Protocol*)protocol;
- + (id)moduleProviderForProtocol:(Protocol *)protocol;
所有組件對外提供的procotol和組件提供的服務(wù)由中間件統(tǒng)一管理,每個組件提供的procotol和服務(wù)是一一對應(yīng)的。
例如:
在JJLoginProvider中:load方法會應(yīng)用啟動的時候調(diào)用,就會在JJProtocolManager進(jìn)行注冊。JJLoginProvider遵守了JJLoginProvider協(xié)議,這樣就可以對外根據(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對應(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];</jjwebviewvcmoduleprotocol>
URL路由方案
URL路由方案最經(jīng)典的就是蘑菇街的路由組件化,通過url的方式將調(diào)用方法,調(diào)用參數(shù),已經(jīng)回調(diào)方法封裝到url中,然后在通過對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)組件化,但是有時候我們會遇到一個圖片編輯模塊,不能傳遞UIImage到對應(yīng)的模塊上去的話,這里我們需要傳個新的參數(shù)進(jìn)去,為了解決這個問題,這樣其實(shí),可以把參數(shù)直接丟給后面的arg處理
- + (nullable id)openURL:(nonnull NSString *)urlString arg:(nullable id)arg error:( NSError*__nullable *__nullable)error completion:(nullable JJRouterCompletion)completion
舉個例子:
- 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)目,這個就是通過url解析和protocol協(xié)議注冊實(shí)現(xiàn)組件化,只是沒有像蘑菇街那樣注冊支持哪些 URL類型。
target-action方案
target-action方案是在學(xué)習(xí)casa大神,CTMediator 的基礎(chǔ)上進(jìn)行的
casa大神認(rèn)為,
- 根本無法表達(dá)非常規(guī)對象,如果用url組件化的話,遇到像UIImage這樣的參數(shù),就需要添加一個參數(shù),才能解決
- URL注冊對于實(shí)施組件化方案是完全不必要的,且通過URL注冊的方式形成的組件化方案,拓展性和可維護(hù)性都會被打折
- 蘑菇街沒有拆分遠(yuǎn)程調(diào)用和本地間調(diào)用
- 蘑菇街必須要在app啟動時注冊URL響應(yīng)者
- //理論上頁面之間的跳轉(zhuǎn)只需 open 一個 URL 即可。所以對于一個組件來說,只要定義「支持哪些 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í)例以及對應(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í)行邏輯時的充要條件是否包含option數(shù)據(jù)。傳入URL之后,CTMediator通過解析URL,將請求路由到對應(yīng)的target和action,隨后的過程就變成了上面說過的本地應(yīng)用調(diào)用的過程了,最終完成響應(yīng)。
針對請求的路由操作很少會采用本地文件記錄路由表的方式,服務(wù)端經(jīng)常處理這種業(yè)務(wù),在服務(wù)端領(lǐng)域基本上都是通過正則表達(dá)式來做路由解析。App中做路由解析可以做得簡單點(diǎn),制定URL規(guī)范就也能完成,最簡單的方式就是scheme://target/action這種,簡單做個字符串處理就能把target和action信息從URL中提取出來了。
舉個例子:
- /**
- 這里是登錄模塊的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路由方案更加的清晰,不過這個還是各取所需吧
接下來,target-action的核心代碼就是:
- /**
- if ([target respondsToSelector:action])
- 判斷target能否響應(yīng)action方法,只要能夠就執(zhí)行這段核心代碼,
- 核心代碼的主要功能:
- **/
- - (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
- {
- //// 創(chuàng)建一個函數(shù)簽名,這個簽名可以是任意的,但需要注意,簽名函數(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 兩個參數(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í)例以及對應(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ī)上的端口號(http 的默認(rèn)端口號是 80)
- path - 定義服務(wù)器上的路徑(如果省略,則文檔必須位于網(wǎng)站的根目錄中)。
- filename - 定義文檔/資源的名稱