iOS组件化不只是架构师的事

移动开发
为什么要组件化,在看过很多优秀的文章后,你一定会问这个问题,组件化能给我们带来多大的好处?作为一个小公司而言,涉及组件化的机会很少,没有大厂的工作经验,也很难将组件化理解的很透彻。

iOS组件化曾今在业界是多么的火热的话题,现在在少有人再次提及这个的话题。网上也很多关于组件化的文章和思想,最经典的要是casa大神和蘑菇街关于组件化的论战。想想曾经看到这些文章的时候,觉得组件化是多么优秀的思想,觉得他们说的都有道理,而casa大神应该在很多思想上给了我等码农很多灵感。而两位大神架构师级别的论剑是否让你真正理解到组件化的重要性。是否让你在内心深处产生共鸣,最 近看到一个项目让我对组件化多了些思考。

[[249145]]

一、为什么要组件化,组件化到底有什么好处?

为什么要组件化,在看过很多优秀的文章后,你一定会问这个问题,组件化能给我们带来多大的好处?作为一个小公司而言,涉及组件化的机会很少,没有大厂的工作经验,也很难将组件化理解的很透彻。可能以为我们的业务模块还不够多,或者说,我们没有理解到他的好处,其实组件化***的好处就是,每个组件,每个模块都可能单独成一个app,具有自己的生命周期。这样就可以分割成不同的业务组模块去处理,之前听说京东,有团队专门负责消息模块,有团队专门负责广告模块,有团队专门负责发现模块,这是你就会发现如果没有很好的组件化思想,这样的多团队合作就非常的困难,已经很难维护好这个项目的开发迭代。说了这么多,到底组件化是什么样子的呢?那我跟着我的脚步,学习分析,探讨下。

二、组件化的核心思想

组件化的话的核心思想,也是我们进行组件化的基础框架,就是通过怎么样的方式实现组件化,或者如何从架构层,业务层多个层次实现架构呢。要想实现组件化,其实就是建立一个中间转换的工具。你也可以理解为路由,通过路由的思想实现跨业务的数据沟通,从而一定程度上的降低各层数据的耦合。减少各个业务层等层级的import发生的耦合。

三、目前实现的组件化的方式

目前实现一般有下面三种思想:

  1. Procotol方案
  2. URL路由方案
  3. target-action方案

Procotol协议注册方案

关于procotol协议注册方案看人用的比较少,也很少看到有人分享,我也是在这个项目中看到,就研究了一下。通过JJProtocolManager 作为中间转化。 

  1. + (void)registerModuleProvider:(id)provider forProtocol:(Protocol*)protocol;  
  2. + (id)moduleProviderForProtocol:(Protocol *)protocol; 

所有组件对外提供的procotol和组件提供的服务由中间件统一管理,每个组件提供的procotol和服务是一一对应的。

例如:

在JJLoginProvider中:load方法会应用启动的时候调用,就会在JJProtocolManager进行注册。JJLoginProvider遵守了JJLoginProvider协议,这样就可以对外根据业务需求提供一些方法。 

  1. + (void)load 
  2.     [JJProtocolManager registerModuleProvider:[self new] forProtocol:@protocol(JJLoginProtocol)]; 
  3. - (UIViewController *)viewControllerWithInfo:(id)userInfo needNew:(BOOL)needNew callback:(JJModuleCallbackBlock)callback{ 
  4.     CLoginViewController *vc = [[CLoginViewController alloc] init]; 
  5.     vc.jj_moduleCallbackBlock = callback; 
  6.     vc.jj_moduleUserInfo = userInfo; 
  7.     return vc; 

这样就可以在需要登录业务模块的地方,通过JJProtocolManager取出JJLoginProtocol对应的服务提供者JJLoginProvider,直接获取。如下: 

  1. id<jjwebviewvcmoduleprotocol> provider = [JJProtocolManager moduleProviderForProtocol:@protocol(JJWebviewVCModuleProtocol)]; 
  2.    UIViewController *vc =[provider viewControllerWithInfo:obj needNew:YES callback:^(id info) { 
  3.        if (callback) { 
  4.            callback(info); 
  5.        } 
  6.    }]; 
  7.    vc.hidesBottomBarWhenPushed = YES; 
  8.    [self.currentNav pushViewController:vc animated:YES];</jjwebviewvcmoduleprotocol> 

URL路由方案

URL路由方案最经典的就是蘑菇街的路由组件化,通过url的方式将调用方法,调用参数,已经回调方法封装到url中,然后在通过对url的解析获取到方法名,参数,***通过消息转发机制调用方法。

下面是蘑菇街的路由方式:(这里要是想详细了解,可以到蘑菇街的路由组件化 中具体学习) 

  1. [MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) { 
  2.     NSNumber *id = routerParameters[@"id"]; 
  3.     // create view controller with id 
  4.     // push view controller 
  5. }]; 

首页只需调用 [MGJRouter openURL:@"mgj://detail?id=404"] 就可以打开相应的详情页。

这里可以看到,我们通过url短链的方式,通过将参数拼接到url query部分,这样就可以,通过这样解析url中的scheme,host,path,query获取到调转什么要的控制器,需要传什么什么样的参数,从而push或者present新页面。

解析scheme,host,path核心代码: 

  1. NSString *scheme = [nsUrl scheme];//解析scheme 
  2.    NSString *module = [nsUrl host]; 
  3.    NSString *action = [[nsUrl path] stringByReplacingOccurrencesOfString:@"/" withString:@"_"]; 
  4.    if (action && [action length] && [action hasPrefix:@"_"]) { 
  5.        action = [action stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:@""]; 
  6.    } 
  7.   
  8.    NSString *query = nil; 
  9.    NSArray* pathInfo = [nsUrl.absoluteString componentsSeparatedByString:@"?"]; 
  10.    if (pathInfo.count > 1) { 
  11.        query = [pathInfo objectAtIndex:1]; 
  12.    } 

解析query的核心代码: 

  1. NSMutableDictionary *parameters = nil; 
  2. NSString *parametersString = query; 
  3. NSArray *paramStringArr = [parametersString componentsSeparatedByString:@"&"]; 
  4. if (paramStringArr && [paramStringArr count]>0) { 
  5.     parameters = [NSMutableDictionary dictionary]; 
  6.     for (NSString* paramString in paramStringArr) { 
  7.         NSArray *paramArr = [paramString componentsSeparatedByString:@"="]; 
  8.         if (paramArr.count > 1) { 
  9.             NSString *key = [paramArr objectAtIndex:0]; 
  10.             NSString *value = [paramArr objectAtIndex:1]; 
  11.             parameters[key] = [JJRouter unescapeURIComponent:value]; 
  12.         } 
  13.     } 
  14. return parameters; 

通过这样的方式,我们就可以实现组件化,但是有时候我们会遇到一个图片编辑模块,不能传递UIImage到对应的模块上去的话,这里我们需要传个新的参数进去,为了解决这个问题,这样其实,可以把参数直接丢给后面的arg处理

  1. + (nullable id)openURL:(nonnull NSString *)urlString arg:(nullable id)arg error:( NSError*__nullable *__nullable)error completion:(nullable JJRouterCompletion)completion 

举个例子: 

  1.     Action *action = [Action new]; 
  2.            action.type = JJ_WebView; 
  3.            Params *params = [[Params alloc] init]; 
  4.            //            params.pageID = JJ_LOGIN; 
  5.            action.params = params; 
  6.            NSDictionary *parms = @{Jump_Key_Action:action, Jump_Key_Param : @{WebUrlString:@"http://www.baidu.com",Name:@"小二"}, Jump_Key_Callback:[JJFunc callback:^(id  _Nullable object) { 
  7.                NSLog(@"%@",object); 
  8.            }]}; 
  9. //            ActionJump(parms); 
  10.              
  11.            [JJRouter openURL:@"router://JJActionService/showWebVC" arg: parms error:nil completion:parms[Jump_Key_Callback]]; 
  12.        } 

我看的项目,这个就是通过url解析和protocol协议注册实现组件化,只是没有像蘑菇街那样注册支持哪些 URL类型。

target-action方案

target-action方案是在学习casa大神,CTMediator 的基础上进行的

casa大神认为,

  1. 根本无法表达非常规对象,如果用url组件化的话,遇到像UIImage这样的参数,就需要添加一个参数,才能解决
  2. URL注册对于实施组件化方案是完全不必要的,且通过URL注册的方式形成的组件化方案,拓展性和可维护性都会被打折
  3. 蘑菇街没有拆分远程调用和本地间调用
  4. 蘑菇街必须要在app启动时注册URL响应者 
  1. //理论上页面之间的跳转只需 open 一个 URL 即可。所以对于一个组件来说,只要定义「支持哪些 URL」即可,比如详情页,大概可以这么做的  
  2. [MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) { 
  3.    NSNumber *id = routerParameters[@"id"]; 
  4.    // create view controller with id 
  5.    // push view controller 
  6. }]; 

而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中提取出来了。

举个例子: 

  1. /** 
  2. 这里是登录模块的target 
  3. **/ 
  4. #import "CTMediator+ModuleLogin.h" 
  5. NSString * const kCTMediatorTargetA = @"A"
  6. NSString * const kCTMediatorActionLoginViewController = @"showLoginController"
  7. @implementation CTMediator (ModuleLogin) 
  8. - (UIViewController *)push_viewControllerForLogin 
  9.    UIViewController *vc = [self performTarget:kCTMediatorTargetA action:kCTMediatorActionLoginViewController params:nil shouldCacheTarget:NO]; 
  10.      
  11.    if ([vc isKindOfClass:[UIViewController class]]) { 
  12.        // view controller 交付出去之后,可以由外界选择是push还是present 
  13.        return vc; 
  14.    } else { 
  15.        // 这里处理异常场景,具体如何处理取决于产品 
  16.        return [[UIViewController alloc] init]; 
  17.    } 

 

  1. /** 
  2. 登录模块的action 
  3. **/ 
  4. - (UIViewController *)Action_showLoginController:(NSDictionary *)param 
  5.    JJLoginViewController *vc =[[JJLoginViewController alloc] init]; 
  6.      
  7.    return vc; 

看上去,target-action路由方案更加的清晰,不过这个还是各取所需吧

接下来,target-action的核心代码就是: 

  1. /** 
  2. if ([target respondsToSelector:action]) 
  3. 判断target能否响应action方法,只要能够就执行这段核心代码, 
  4. 核心代码的主要功能: 
  5. **/ 
  6. - (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params 
  7.    //// 创建一个函数签名,这个签名可以是任意的,但需要注意,签名函数的参数数量要和调用的一致。 
  8.    NSMethodSignature* methodSig = [target methodSignatureForSelector:action]; 
  9.    if(methodSig == nil) { 
  10.        return nil; 
  11.    } 
  12. //    获取返回类型 
  13.    const char* retType = [methodSig methodReturnType]; 
  14. //判断返回值类型 
  15.    if (strcmp(retType, @encode(void)) == 0) { 
  16. // 通过签名初始化 
  17.        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; 
  18. //如果此消息有参数需要传入,那么就需要按照如下方法进行参数设置,需要注意的是,atIndex的下标必须从2开始。原因为:0 1 两个参数已经被target 和selector占用 
  19.        [invocation setArgument:¶ms atIndex:2]; 
  20. // 设置selector 
  21.        [invocation setSelector:action]; 
  22. // 设置target 
  23.        [invocation setTarget:target]; 
  24. //消息调用 
  25.        [invocation invoke]; 
  26.        return nil; 
  27.    } 
  28.    if (strcmp(retType, @encode(NSInteger)) == 0) { 
  29.        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; 
  30.        [invocation setArgument:¶ms atIndex:2]; 
  31.        [invocation setSelector:action]; 
  32.        [invocation setTarget:target]; 
  33.        [invocation invoke]; 
  34.        NSInteger result = 0; 
  35.        [invocation getReturnValue:&result]; 
  36.        return @(result); 
  37.    } 
  38.    if (strcmp(retType, @encode(BOOL)) == 0) { 
  39.        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; 
  40.        [invocation setArgument:¶ms atIndex:2]; 
  41.        [invocation setSelector:action]; 
  42.        [invocation setTarget:target]; 
  43.        [invocation invoke]; 
  44.        BOOL result = 0; 
  45.        [invocation getReturnValue:&result]; 
  46.        return @(result); 
  47.    } 
  48.    if (strcmp(retType, @encode(CGFloat)) == 0) { 
  49.        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; 
  50.        [invocation setArgument:¶ms atIndex:2]; 
  51.        [invocation setSelector:action]; 
  52.        [invocation setTarget:target]; 
  53.        [invocation invoke]; 
  54.        CGFloat result = 0; 
  55.        [invocation getReturnValue:&result]; 
  56.        return @(result); 
  57.    } 
  58.    if (strcmp(retType, @encode(NSUInteger)) == 0) { 
  59.        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; 
  60.        [invocation setArgument:¶ms atIndex:2]; 
  61.        [invocation setSelector:action]; 
  62.        [invocation setTarget:target]; 
  63.        [invocation invoke]; 
  64.        NSUInteger result = 0; 
  65.        [invocation getReturnValue:&result]; 
  66.        return @(result); 
  67.    } 
  68. #pragma clang diagnostic push 
  69. #pragma clang diagnostic ignored "-Warc-performSelector-leaks" 
  70.    return [target performSelector:action withObject:params]; 
  71. #pragma clang diagnostic pop 

总结:

CTMediator根据获得的target和action信息,通过objective-C的runtime转化生成target实例以及对应的action选择子,然后最终调用到目标业务提供的逻辑,完成需求。

下面是三种方式的代码实现Git的地址:

https://github.com/lumig/JJRouterDemo

彩蛋: 

  1. // url 编码格式 
  2. foo://example.com:8042/over/there?name=ferret#nose 
  3. \_/ \______________/ \________/\_________/ \__/ 
  4. |         |              |         |        | 
  5. scheme authority         path      query   fragment 
  6. scheme://host.domain:port/path/filename 
  7. scheme - 定义因特网服务的类型。最常见的类型是 http 
  8. host - 定义域主机(http 的默认主机是 www) 
  9. domain - 定义因特网域名,比如 w3school.com.cn 
  10. :port - 定义主机上的端口号(http 的默认端口号是 80) 
  11. path - 定义服务器上的路径(如果省略,则文档必须位于网站的根目录中)。 
  12. filename - 定义文档/资源的名称 

 

责任编辑:未丽燕 来源: 简书
相关推荐

2015-11-24 10:05:07

私有云虚拟化负载迁移

2017-03-25 21:13:38

JavaScript排序

2011-04-28 20:21:44

和信创天终端管理虚拟终端管理系统

2010-08-05 09:29:08

jQuery

2013-04-25 13:58:15

编程

2018-03-13 15:00:22

智慧交通高铁无人驾驶

2021-11-05 11:17:45

互联网996大厂

2018-06-28 18:10:41

华为

2021-01-06 10:51:39

云计算云服务IT

2015-12-15 17:19:55

戴尔云计算

2016-10-13 18:06:09

云计算多云模型

2015-02-04 09:45:40

2012-09-18 10:20:17

2011-11-17 13:25:43

垃圾邮件

2011-09-15 13:25:02

2011-06-29 16:29:19

2018-06-27 17:24:24

华为

2015-03-31 09:28:28

Hadoop大数据技术大数据未来道路

2022-11-02 11:48:03

Vanilla OSGNOMEUbuntu

2021-07-26 22:33:41

切片结构体代码
点赞
收藏

51CTO技术栈公众号