为什么要组件化, 在看过很多优秀的文章后, 你一定会问这个问题, 组件化能给我们带来多大的好处? 作为一个小公司而言, 涉及组件化的机会很少, 没有大厂的工作经验, 也很难将组件化理解的很透彻.
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 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 路由方案最经典的就是蘑菇街的路由组件化, 通过 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 - 定义文档 / 资源的名称
来源: http://mobile.51cto.com/hot-586795.htm