iOS組件化不只是架構師的事
iOS組件化曾今在業界是多么的火熱的話題,現在在少有人再次提及這個的話題。網上也很多關于組件化的文章和思想,最經典的要是casa大神和蘑菇街關于組件化的論戰。想想曾經看到這些文章的時候,覺得組件化是多么優秀的思想,覺得他們說的都有道理,而casa大神應該在很多思想上給了我等碼農很多靈感。而兩位大神架構師級別的論劍是否讓你真正理解到組件化的重要性。是否讓你在內心深處產生共鳴,最 近看到一個項目讓我對組件化多了些思考。
一、為什么要組件化,組件化到底有什么好處?
為什么要組件化,在看過很多優秀的文章后,你一定會問這個問題,組件化能給我們帶來多大的好處?作為一個小公司而言,涉及組件化的機會很少,沒有大廠的工作經驗,也很難將組件化理解的很透徹。可能以為我們的業務模塊還不夠多,或者說,我們沒有理解到他的好處,其實組件化***的好處就是,每個組件,每個模塊都可能單獨成一個app,具有自己的生命周期。這樣就可以分割成不同的業務組模塊去處理,之前聽說京東,有團隊專門負責消息模塊,有團隊專門負責廣告模塊,有團隊專門負責發現模塊,這是你就會發現如果沒有很好的組件化思想,這樣的多團隊合作就非常的困難,已經很難維護好這個項目的開發迭代。說了這么多,到底組件化是什么樣子的呢?那我跟著我的腳步,學習分析,探討下。
二、組件化的核心思想
組件化的話的核心思想,也是我們進行組件化的基礎框架,就是通過怎么樣的方式實現組件化,或者如何從架構層,業務層多個層次實現架構呢。要想實現組件化,其實就是建立一個中間轉換的工具。你也可以理解為路由,通過路由的思想實現跨業務的數據溝通,從而一定程度上的降低各層數據的耦合。減少各個業務層等層級的import發生的耦合。
三、目前實現的組件化的方式
目前實現一般有下面三種思想:
- Procotol方案
- URL路由方案
- target-action方案
Procotol協議注冊方案
關于procotol協議注冊方案看人用的比較少,也很少看到有人分享,我也是在這個項目中看到,就研究了一下。通過JJProtocolManager 作為中間轉化。
- + (void)registerModuleProvider:(id)provider forProtocol:(Protocol*)protocol;
- + (id)moduleProviderForProtocol:(Protocol *)protocol;
所有組件對外提供的procotol和組件提供的服務由中間件統一管理,每個組件提供的procotol和服務是一一對應的。
例如:
在JJLoginProvider中:load方法會應用啟動的時候調用,就會在JJProtocolManager進行注冊。JJLoginProvider遵守了JJLoginProvider協議,這樣就可以對外根據業務需求提供一些方法。
- + (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;
- }
這樣就可以在需要登錄業務模塊的地方,通過JJProtocolManager取出JJLoginProtocol對應的服務提供者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路由方案最經典的就是蘑菇街的路由組件化,通過url的方式將調用方法,調用參數,已經回調方法封裝到url中,然后在通過對url的解析獲取到方法名,參數,***通過消息轉發機制調用方法。
下面是蘑菇街的路由方式:(這里要是想詳細了解,可以到蘑菇街的路由組件化 中具體學習)
- [MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) {
- NSNumber *id = routerParameters[@"id"];
- // create view controller with id
- // push view controller
- }];
首頁只需調用 [MGJRouter openURL:@"mgj://detail?id=404"] 就可以打開相應的詳情頁。
這里可以看到,我們通過url短鏈的方式,通過將參數拼接到url query部分,這樣就可以,通過這樣解析url中的scheme,host,path,query獲取到調轉什么要的控制器,需要傳什么什么樣的參數,從而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;
通過這樣的方式,我們就可以實現組件化,但是有時候我們會遇到一個圖片編輯模塊,不能傳遞UIImage到對應的模塊上去的話,這里我們需要傳個新的參數進去,為了解決這個問題,這樣其實,可以把參數直接丟給后面的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]];
- }
我看的項目,這個就是通過url解析和protocol協議注冊實現組件化,只是沒有像蘑菇街那樣注冊支持哪些 URL類型。
target-action方案
target-action方案是在學習casa大神,CTMediator 的基礎上進行的
casa大神認為,
- 根本無法表達非常規對象,如果用url組件化的話,遇到像UIImage這樣的參數,就需要添加一個參數,才能解決
- URL注冊對于實施組件化方案是完全不必要的,且通過URL注冊的方式形成的組件化方案,拓展性和可維護性都會被打折
- 蘑菇街沒有拆分遠程調用和本地間調用
- 蘑菇街必須要在app啟動時注冊URL響應者
- //理論上頁面之間的跳轉只需 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來完成調用。這套組件化方案將遠程應用調用和本地應用調用做了拆分,而且是由本地應用調用為遠程應用調用提供服務,與蘑菇街方案正好相反。
調用方式:
先說本地應用調用,本地組件A在某處調用[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]向CTMediator發起跨組件調用,CTMediator根據獲得的target和action信息,通過objective-C的runtime轉化生成target實例以及對應的action選擇子,然后最終調用到目標業務提供的邏輯,完成需求。
在遠程應用調用中,遠程應用通過openURL的方式,由iOS系統根據info.plist里的scheme配置找到可以響應URL的應用(在當前我們討論的上下文中,這就是你自己的應用),應用通過AppDelegate接收到URL之后,調用CTMediator的openUrl:方法將接收到的URL信息傳入。當然,CTMediator也可以用openUrl:options:的方式順便把隨之而來的option也接收,這取決于你本地業務執行邏輯時的充要條件是否包含option數據。傳入URL之后,CTMediator通過解析URL,將請求路由到對應的target和action,隨后的過程就變成了上面說過的本地應用調用的過程了,最終完成響應。
針對請求的路由操作很少會采用本地文件記錄路由表的方式,服務端經常處理這種業務,在服務端領域基本上都是通過正則表達式來做路由解析。App中做路由解析可以做得簡單點,制定URL規范就也能完成,最簡單的方式就是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 {
- // 這里處理異常場景,具體如何處理取決于產品
- 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能否響應action方法,只要能夠就執行這段核心代碼,
- 核心代碼的主要功能:
- **/
- - (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
- {
- //// 創建一個函數簽名,這個簽名可以是任意的,但需要注意,簽名函數的參數數量要和調用的一致。
- 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];
- //如果此消息有參數需要傳入,那么就需要按照如下方法進行參數設置,需要注意的是,atIndex的下標必須從2開始。原因為:0 1 兩個參數已經被target 和selector占用
- [invocation setArgument:¶ms atIndex:2];
- // 設置selector
- [invocation setSelector:action];
- // 設置target
- [invocation setTarget:target];
- //消息調用
- [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
- }
總結:
CTMediator根據獲得的target和action信息,通過objective-C的runtime轉化生成target實例以及對應的action選擇子,然后最終調用到目標業務提供的邏輯,完成需求。
下面是三種方式的代碼實現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 - 定義因特網服務的類型。最常見的類型是 http
- host - 定義域主機(http 的默認主機是 www)
- domain - 定義因特網域名,比如 w3school.com.cn
- :port - 定義主機上的端口號(http 的默認端口號是 80)
- path - 定義服務器上的路徑(如果省略,則文檔必須位于網站的根目錄中)。
- filename - 定義文檔/資源的名稱