iOS組件化不只是架構(gòu)師的事

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:&params 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:&params 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:&params 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:&params 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:&params 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 - 定義文檔/資源的名稱
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市署鸡,隨后出現(xiàn)的幾起案子案糙,更是在濱河造成了極大的恐慌,老刑警劉巖靴庆,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件时捌,死亡現(xiàn)場離奇詭異,居然都是意外死亡炉抒,警方通過查閱死者的電腦和手機(jī)奢讨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來焰薄,“玉大人拿诸,你說我怎么就攤上這事∪” “怎么了亩码?”我有些...
    開封第一講書人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長野瘦。 經(jīng)常有香客問我描沟,道長,這世上最難降的妖魔是什么鞭光? 我笑而不...
    開封第一講書人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任吏廉,我火速辦了婚禮,結(jié)果婚禮上惰许,老公的妹妹穿的比我還像新娘席覆。我一直安慰自己,他們只是感情好汹买,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開白布佩伤。 她就那樣靜靜地躺著,像睡著了一般卦睹。 火紅的嫁衣襯著肌膚如雪畦戒。 梳的紋絲不亂的頭發(fā)上方库,一...
    開封第一講書人閱讀 51,287評(píng)論 1 301
  • 那天结序,我揣著相機(jī)與錄音,去河邊找鬼纵潦。 笑死徐鹤,一個(gè)胖子當(dāng)著我的面吹牛垃环,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播返敬,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼遂庄,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了劲赠?” 一聲冷哼從身側(cè)響起涛目,我...
    開封第一講書人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎凛澎,沒想到半個(gè)月后霹肝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡塑煎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年沫换,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片最铁。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡讯赏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出冷尉,到底是詐尸還是另有隱情漱挎,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布雀哨,位于F島的核電站识樱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏震束。R本人自食惡果不足惜怜庸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望垢村。 院中可真熱鬧割疾,春花似錦、人聲如沸嘉栓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽侵佃。三九已至麻昼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間馋辈,已是汗流浹背抚芦。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人叉抡。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓尔崔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親褥民。 傳聞我的和親對(duì)象是個(gè)殘疾皇子季春,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容