这篇文章主要是我近段时间针对市面上存在的一些组件化方案的调研之后, 再经过自己的反思和总结写的, 博客中部分文字和图借鉴自下面的博客各位看官大爷就当做一篇读书笔记来看即可, 主要是参考了如下几篇文章, 另外零零散散的也看了一些其他资料, 但是大多都是相似的
蘑菇街组件化之路
iOS 应用架构谈 组件化方案
iOS 组件化 路由设计思路分析
滴滴 iOS 的组件化实践与优化
iOS 组件化方案
iOS 组件化方案探索
掌上链家组件化探索历程
京东 iOS 客户端组件管理实践
看上去各家都是各显神通, 都有自己的技术方案, 但是实际上都可以归类到如下两种方案:
利用 runtime 实现的 target-action 方法
利用 url-scheme 方案
目前市面上流行的组件化方案都是通过 url-scheme 实现的, 包括很多开源的组件化的库都是如此, 只有 casa 的方案独树一帜, 是通过 Target-Action 实现的
URL-Scheme 库:
- JLRoutes
- routable-ios
- HHRouter
- MGJRouter
Target-Action 库:
CTMediator
上面这些第三方组件库的具体对比, 大家可以参考霜神的这篇博客:
iOS 组件化 路由设计思路分析
URL-Sheme 方案一般都是各个组件把自己可以提供的服务通过 url 的形式注册到一个中心管理器, 然后调用发就可以通过 openURL 的方式来打开这个 url, 然后中心管理器解析这个 url, 把请求转发到相应的组件去执行
Target-Action 方案利用了 OC 的 runtime 特性, 无需注册, 直接在原有的组件之外加一层 wrapper, 把对外提供的服务都抽离到该层然后通过 runtime 的
TARGET performSelector: ACTION withObject: PARAMS
找到对应的组件, 执行方法和传递参数
就我个人而言, 我还是比较推荐 target-action 方案, 具体原因我们下面会进一步分析
为何要组件化
在做一件事之前我们一般都要搞清楚为什么要这么做, 好处是什么, 有哪些坑, 这样才会有一个整体的认识, 然后再决定要不要做同样我们也要搞清楚到底需不需要实施组件化, 那么就要先搞清楚什么是组件
组件的定义
组件是由一个或多个类构成, 能完整描述一个业务场景, 并能被其他业务场景复用的功能单位组件就像是 PC 时代个人组装电脑时购买的一个个部件, 比如内存, 硬盘, CPU, 显示器等, 拿出其中任何一个部件都能被其他的 PC 所使用
所以组件可以是个广义上的概念, 并不一定是页面跳转, 还可以是其他不具备 UI 属性的服务提供者, 比如日志服务, VOIP 服务, 内存管理服务等等说白了我们目标是站在更高的维度去封装功能单元对这些功能单元进行进一步的分类, 才能在具体的业务场景下做更合理的设计
组件化的优点
纵观目前的已经在实施组件化的团队来看, 大家的一般发展路径都是: 前期项目小, 需要快速迭代抢占市场, 大家都是用传统的 MVC 架构去开发项目等到后期项目越来越大, 开发人数越来越多, 会发现传统的开发方式导致代码管理混乱, 发布集成测试越来越麻烦, 被迫走向组件化的道路
其实组件化也不是完全必须的, 如果你的团队只是开发一个小项目, 团队人数小于 10 个人, 产品线也就是两三条, 那么完全可以用传统开发方式来开发但是如果你的团队在不断发展, 产品线也越来越多的时候, 预计后期可能会更多的时候, 那么最好尽早把组件化提上议程
摘自 casa 的建议:
组件化方案在 App 业务稳定, 且规模 (业务规模和开发团队规模) 增长初期去实施非常重要, 它助于将复杂 App 分而治之, 也有助于多人大型团队的协同开发但组件化方案不适合在业务不稳定的情况下过早实施, 至少要等产品已经经过 MVP 阶段时才适合实施组件化因为业务不稳定意味着链路不稳定, 在不稳定的链路上实施组件化会导致将来主业务产生变化时, 全局性模块调度和重构会变得相对复杂
其实组件化也没有多么高大上, 和我们之前说的模块化差不多, 就是把一些业务基础功能剥离, 划分为一个个的模块, 然后通过 pods 的方式管理而已, 同时要搭配一套后台的自动集成发布测试流程
一般当项目越来越大的时候, 无可避免的会遇到如下的痛点:
代码冲突多, 编译慢
每一次拉下代码开发功能, 开发完成准备提交代码时, 往往有其他工程师提交了代码, 需要重新拉去代码合并后再提交, 即使开发一个很小的功能, 也需要在整个工程里做编译和调试, 效率较低
迭代速度慢, 耦合比较严重, 无法单独测试
各个业务模块之间互相引入, 耦合严重每次需要发版时, 所有的业务线修改都需要全部回归, 然后审查看是否出错, 耗费大量时间业务线之间相互依赖, 可能会导致一个业务线必须等待另外一个业务线开发完某个功能才可以接着开发, 无法并行开发还有一个问题, 就是耦合导致无法单独测试某个业务线, 可能需要等到所有业务线开发完毕, 才能统一测试, 浪费测试资源
为了解决上述痛点, 组件化应运而生, 总体来说, 组件化就是把整个项目进行拆分, 分成一个个单独的可独立运行的组件, 分开管理, 减少依赖 完成组件化之后, 一般可达到如下效果:
加快编译速度, 可以把不会经常变动的组件做成静态库, 同时每个组件可以独立编译, 不依赖于主工程或者其他组件
每个组件都可以选择自己擅长的开发模式(MVC / MVVM / MVP)
可以单独测试每个组件
多条业务线可以并行开发, 提高开发效率
如何组件化
当我们确定需要对项目进行组件化了, 我们第一个要解决的问题就是如何拆分组件这是一个见仁见智的问题, 没有太明确的划分边界, 大致做到每个组件只包含一个功能即可, 具体实施还是要根据实际情况权衡
当我们写一个类的时候, 我们会谨记高内聚, 低耦合的原则去设计这个类, 当涉及多个类之间交互的时候, 我们也会运用 SOLID 原则, 或者已有的设计模式去优化设计, 但在实现完整的业务模块的时候, 我们很容易忘记对这个模块去做设计上的思考, 粒度越大, 越难做出精细稳定的设计, 我暂且把这个粒度认为是组件的粒度
组件可以是个广义上的概念, 并不一定是页面跳转, 还可以是其他不具备 UI 属性的服务提供者, 比如日志服务, VOIP 服务, 内存管理服务等等说白了我们目标是站在更高的维度去封装功能单元, 把多个功能单元组合在一起形成一个更大的功能单元, 也就是组件对这些功能单元进行进一步的分类, 才能在具体的业务场景下做更合理的设计
下面的组件划分粒度, 大家可以借鉴一下
组件化前后对比
iOS 里面的组件化主要是通过 cocopods 把组件打包成单独的私有 pod 库来进行管理, 这样就可以通过 podfile 文件, 进行动态的增删和版本管理了
下面是链家 APP 在实行组件化前后的对比
可以看到传统的 MVC 架构把所有的模块全部糅合在一起, 是一种分布式的管理方法, 耦合严重, 当业务线过多的时候就会出现我们上面说的问题 而下图的组件化方式是一种中心 Mediator 的方式, 让所有业务组件都分开, 然后都依赖于 Mediator 进行统一管理, 减少耦合
组件化后, 代码分类也更符合人类大脑的思考方式
组件化方案对比分析
组件化如何解决现有工程问题
传统模式的组件之间的跳转都是通过直接 import, 当模块比较少的时候这个方式看起来没啥问题但到了项目越来越庞大, 这种模式会导致每个模块都离不开其他模块, 互相依赖耦合严重这种方式是分布式的处理方式, 每个组件都是处理和自己相关的业务管理起来很混乱, 如下图所示:
(借用霜神的几张图)
那么按照人脑的思维方式, 改成如下这种中心化的方式更加清晰明了:
但是上面这个图虽然看起来比刚开始好了许多, 但是每个组件还是和 mediator 双向依赖, 如果改成如下图所示就完美了:
这个时候看起来就舒服多了, 每个组件只需要自己管好自己就完了, 然后由 mediator 负责在各个组件中间进行转发或者跳转, perfect~~ 那么如何实现这个架构呢? 只要解决下面两个问题就好了:
mediator 作为中间件, 需要通过某种方式找到每个组件, 并能调用组件的方法
每个组件如何得知其他组件提供了哪些方法? 只有这样才可以调用对方嘛
原始工程
假设我们现有工程里面有两个组件 AB, 功能很简单, 如下所示
- #import <UIKit/UIKit.h>
- @interface A_VC : UIViewController
- -(void)action_A:(NSString*)para1;
- @end
- ==================================
- #import "A_VC.h"
- @implementation A_VC
- -(void)action_A:(NSString*)para1 {
- NSLog(@"call action_A %@",para1);
- }
- @end
- #import <UIKit/UIKit.h>
- @interface B_VC : UIViewController
- -(void)action_B:(NSString*)para1 para2:(NSInteger)para2;
- @end
- ====================
- #import "B_VC.h"
- @implementation B_VC
- -(void)action_B:(NSString*)para1 para2:(NSInteger)para2{
- NSLog(@"call action_B %@---%zd",para1,para2);
- }
- @end
如果是传统做法, AB 要调用对方的功能, 就会直接 import 对方, 然后初始化, 接着调用方法现在我们对他们实行组件化, 改成如上图所示的 mediator 方式
target-action 方案
该方案借助 OC 的 runtime 特性, 实现了服务的自动发现, 无需注册即可实现组件间调用不管是从维护性可读性扩展性方面来讲, 都优于 url-scheme 方案, 也是我比较推崇的组件化方案, 下面我们就来看看该方案如何解决上述两个问题的
Demo 演示
此时 AB 两个组件不用改, 我们需要加一个 mediator, 代码如下所示:
- #import <Foundation/Foundation.h>
- @interface Mediator : NSObject
- -(void)A_VC_Action:(NSString*)para1;
- -(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2;
- + (instancetype)sharedInstance;
- @end
- ===========================================
- #import "Mediator.h"
- @implementation Mediator
- + (instancetype)sharedInstance
- {
- static Mediator *mediator;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- mediator = [[Mediator alloc] init];
- });
- return mediator;
- }
- -(void)A_VC_Action:(NSString*)para1{
- Class cls = NSClassFromString(@"A_VC");
- NSObject *target = [[cls alloc]init];
- [target performSelector:NSSelectorFromString(@"action_A:") withObject:para1];
- }
- -(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2{
- Class cls = NSClassFromString(@"B_VC");
- NSObject *target = [[cls alloc]init];
- [target performSelector:NSSelectorFromString(@"action_B:para2:") withObject:para1 withObject:para2];
- }
- @end
组件 B 调用组件 A, 如下所示:
[[Mediator sharedInstance] A_VC_Action: @"参数 1"];
组件 A 调用组件 B, 如下所示:
[[Mediator sharedInstance] B_VC_Action: @"参数 1"para2: 123];
此时已经可以做到最后一张图所示的效果了, 组件 A,B 依赖 mediator,mediator 不依赖组件 A,B(也不是完全不依赖, 而是把用 runtime 特性把类的引用弱化为了字符串)
反思
看到这里, 大概有人会问, 既然用 runtime 就可以解耦取消依赖, 那还要 Mediator 做什么? 我直接在每个组件里面用 runtime 调用其他组件不就完了吗, 干嘛还要多一个 mediator?
但是这样做会存在如下问题:
调用者写起来很恶心, 代码提示都没有, 参数传递非常恶心, 每次调用者都要查看文档搞清楚每个参数的 key 是什么, 然后自己去组装成一个 NSDictionary 维护这个文档和每次都要组装参数字典很麻烦
当调用的组件不存在的时候, 没法进行统一处理
那么加一个 mediator 的话, 就可以做到:
调用者写起来不恶心, 代码提示也有了, 参数类型明确
Mediator 可以做统一处理, 调用某个组件方法时如果某个组件不存在, 可以做相应操作, 让调用者与组件间没有耦合
改进
聪明的读者可能已经发现上面的 mediator 方案还是存在一个小瑕疵, 受限于 performselector 方法, 最多只能传递两个参数, 如果我想传递多个参数怎么办呢?
答案是使用字典进行传递, 此时我们还需要个组件增加一层 wrapper, 把对外提供的业务全部包装一次, 并且接口的参数全部改成字典 假设我们现在的 B 组件需要接受多个参数, 如下所示:
- - (void) action_B: (NSString * ) para para2: (NSInteger) para2 para3: (NSInteger) para3 para4: (NSInteger) para4 {
- NSLog(@"call action_B %@---%zd---%zd----%zd", para1, para2, para3, para4);
- }
那么此时需要对 B 组件增加一层 wrapper, 如下:
- #import <Foundation/Foundation.h>
- @interface target_B : NSObject
- -(void)B_Action:(NSDictionary*)para;
- @end
- =================
- #import "target_B.h"
- #import "B_VC.h"
- @implementation target_B
- -(void)B_Action:(NSDictionary*)para{
- NSString *para1 = para[@"para1"];
- NSInteger para2 = [para[@"para2"]integerValue];
- NSInteger para3 = [para[@"para3"]integerValue];
- NSInteger para4 = [para[@"para4"]integerValue];
- B_VC *VC = [B_VC new];
- [VC action_B:para1 para2:para2 para3:para3 para4:para4];
- }
- @end
此时 mediator 也需要做相应的更改, 由原来直接调用组件 B, 改成了调用 B 的 wrapper 层:
- -(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
- Class cls = NSClassFromString(@"target_B");
- NSObject *target = [[cls alloc]init];
- [target performSelector:NSSelectorFromString(@"B_Action:") withObject:@{@"para1":para1, @"para2":@(para2),@"para3":@(para3),@"para4":@(para4)} ];
- }
现在的组件 A 调用组件 B 的流程如下所示:
此时的项目结构如下:
继续改进
做到这里, 看似比较接近我的要求了, 但是还有有点小瑕疵:
Mediator 每一个方法里都要写 runtime 方法, 格式是确定的, 这是可以抽取出来的
每个组件对外方法都要在 Mediator 写一遍, 组件一多 Mediator 类的长度是恐怖的
接着优化就是 casa 的方案了, 我们来看看如何改进, 直接看代码:
针对第一点, 我们可以抽出公共代码, 当做 mediator:
- #import "CTMediator.h"
- #import <objc/runtime.h>
- @interface CTMediator ()
- @property (nonatomic, strong) NSMutableDictionary *cachedTarget;
- @end
- @implementation CTMediator
- #pragma mark - public methods
- + (instancetype)sharedInstance
- {
- static CTMediator *mediator;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- mediator = [[CTMediator alloc] init];
- });
- return mediator;
- }
- /*
- scheme://[target]/[action]?[params]
- url sample:
- aaa://targetA/actionB?id=1234
- */
- - (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
- {
- NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
- NSString *urlString = [url query];
- for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
- NSArray *elts = [param componentsSeparatedByString:@"="];
- if([elts count] < 2) continue;
- [params setObject:[elts lastObject] forKey:[elts firstObject]];
- }
- // 这里这么写主要是出于安全考虑, 防止黑客通过远程方式调用本地模块这里的做法足以应对绝大多数场景, 如果要求更加严苛, 也可以做更加复杂的安全逻辑
- NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
- if ([actionName hasPrefix:@"native"]) {
- return @(NO);
- }
- // 这个 demo 针对 URL 的路由处理非常简单, 就只是取对应的 target 名字和 method 名字, 但这已经足以应对绝大部份需求如果需要拓展, 可以在这个方法调用之前加入完整的路由逻辑
- id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
- if (completion) {
- if (result) {
- completion(@{@"result":result});
- } else {
- completion(nil);
- }
- }
- return result;
- }
- - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
- {
- NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
- NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
- Class targetClass;
- NSObject *target = self.cachedTarget[targetClassString];
- if (target == nil) {
- targetClass = NSClassFromString(targetClassString);
- target = [[targetClass alloc] init];
- }
- SEL action = NSSelectorFromString(actionString);
- if (target == nil) {
- // 这里是处理无响应请求的地方之一, 这个 demo 做得比较简单, 如果没有可以响应的 target, 就直接 return 了实际开发过程中是可以事先给一个固定的 target 专门用于在这个时候顶上, 然后处理这种请求的
- return nil;
- }
- if (shouldCacheTarget) {
- self.cachedTarget[targetClassString] = target;
- }
- if ([target respondsToSelector:action]) {
- return [self safePerformAction:action target:target params:params];
- } else {
- // 有可能 target 是 Swift 对象
- actionString = [NSString stringWithFormat:@"Action_%@WithParams:", actionName];
- action = NSSelectorFromString(actionString);
- if ([target respondsToSelector:action]) {
- return [self safePerformAction:action target:target params:params];
- } else {
- // 这里是处理无响应请求的地方, 如果无响应, 则尝试调用对应 target 的 notFound 方法统一处理
- SEL action = NSSelectorFromString(@"notFound:");
- if ([target respondsToSelector:action]) {
- return [self safePerformAction:action target:target params:params];
- } else {
- // 这里也是处理无响应请求的地方, 在 notFound 都没有的时候, 这个 demo 是直接 return 了实际开发过程中, 可以用前面提到的固定的 target 顶上的
- [self.cachedTarget removeObjectForKey:targetClassString];
- return nil;
- }
- }
- }
- }
- - (void)releaseCachedTargetWithTargetName:(NSString *)targetName
- {
- NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
- [self.cachedTarget removeObjectForKey:targetClassString];
- }
- #pragma mark - private methods
- - (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];
- [invocation setArgument:¶ms atIndex:2];
- [invocation setSelector:action];
- [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
- }
- #pragma mark - getters and setters
- - (NSMutableDictionary *)cachedTarget
- {
- if (_cachedTarget == nil) {
- _cachedTarget = [[NSMutableDictionary alloc] init];
- }
- return _cachedTarget;
- }
- @end
针对第二点, 我们通过把每个组件的对外接口进行分离, 剥离到多个 mediator 的 category 里面, 感官上把本来在一个 mediator 里面实现的对外接口分离到多个 category 里面, 方便管理
下面展示的是个组件 B 添加的 category, 组件 A 类似
- #import "CTMediator.h"
- @interface CTMediator (B_VC_Action)
- -(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4;
- @end
- ====================
- #import "CTMediator+B_VC_Action.h"
- @implementation CTMediator (B_VC_Action)
- -(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
- [self performTarget:@"target_B" action:@"B_Action" params:@{@"para1":para1, @"para2":@(para2),@"para3":@(para3),@"para4":@(para4)} shouldCacheTarget:YES];
- }
- @end
此时调用者只要引入该 category, 然后调用即可, 调用逻辑其实和上面没有拆分出 category 是一样的此时的项目结构如下:
URL-Scheme 方案
这个方案是流传最广的, 也是最多人使用的, 因为 Apple 本身也提供了 url-scheme 功能, 同时 web 端也是通过 URL 的方式进行路由跳转, 那么很自然的 iOS 端就借鉴了该方案
如何实现
Router 实现代码
- #import <Foundation/Foundation.h>
- typedef void (^componentBlock) (NSDictionary *param);
- @interface URL_Roueter : NSObject
- + (instancetype)sharedInstance;
- - (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk;
- - (void)openURL:(NSString *)url withParam:(id)param;
- @end
- ====================
- #import "URL_Roueter.h"
- @interface URL_Roueter()
- @property (nonatomic, strong) NSMutableDictionary *cache;
- @end
- @implementation URL_Roueter
- + (instancetype)sharedInstance
- {
- static URL_Roueter *router;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- router = [[URL_Roueter alloc] init];
- });
- return router;
- }
- -(NSMutableDictionary *)cache{
- if (!_cache) {
- _cache = [NSMutableDictionary new];
- }
- return _cache;
- }
- - (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk {
- [self.cache setObject:blk forKey:urlPattern];
- }
- - (void)openURL:(NSString *)url withParam:(id)param {
- componentBlock blk = [self.cache objectForKey:url];
- if (blk) blk(param);
- }
- @end
组件 A
- #import "A_VC.h"
- #import "URL_Roueter.h"
- @implementation A_VC
- // 把自己对外提供的服务 (block) 用 url 标记, 注册到路由管理中心组件
- +(void)load{
- [[URL_Roueter sharedInstance]registerURLPattern:@"test://A_Action" toHandler:^(NSDictionary* para) {
- NSString *para1 = para[@"para1"];
- [[self new] action_A:para1];
- }];
- }
- -(void)viewDidLoad{
- [super viewDidLoad];
- UIButton *btn = [UIButton new];
- [btn setTitle:@"调用组件 B" forState:UIControlStateNormal];
- btn.frame = CGRectMake(100, 100, 100, 50);
- [btn addTarget:self action:@selector(btn_click) forControlEvents:UIControlEventTouchUpInside];
- [btn setBackgroundColor:[UIColor redColor]];
- self.view.backgroundColor = [UIColor blueColor];
- [self.view addSubview:btn];
- }
- // 调用组件 B 的功能
- -(void)btn_click{
- [[URL_Roueter sharedInstance]openURL:@"test://B_Action" withParam:@{@"para1":@"PARA1", @"para2":@(222),@"para3":@(333),@"para4":@(444)}];
- }
- -(void)action_A:(NSString*)para1 {
- NSLog(@"call action_A: %@",para1);
- }
- @end
组件 B 实现的代码类似, 就不在贴了上面都是简化版的实现, 不过核心原理是一样的
从上面的代码可以看出来, 实现原理很简单: 每个组件在自己的 load 方面里面, 把自己对外提供的服务 (回调 block) 通过 url-scheme 标记好, 然后注册到 URL-Router 里面
URL-Router 接受各个组件的注册, 用字典保存了每个组件注册过来的 url 和对应的服务, 只要其他组件调用了 openURL 方法, 就会去这个字典里面根据 url 找到对应的 block 执行(也就是执行其他组件提供的服务)
存在的问题
通过 url-scheme 的方式去做组件化主要存在如下一些问题:
需要专门的管理后台维护
要提供一个文档专门记录每个 url 和服务的对应表, 每次组件改动了都要即使修改, 很麻烦参数的格式不明确, 是个灵活的 dictionary, 同样需要维护一份文档去查这些参数
内存问题
每个组件在初始化的时候都需要要路由管理中心去注册自己提供的服务, 内存里需要保存一份表, 组件多了会有内存问题
混淆了本地调用和远程调用
url-scheme 是 Apple 拿来做 app 之间跳转的, 或者通过 url 方式打开 APP, 但是上述的方案去把他拿来做本地组件间的跳转, 这会产生问题, 大概分为两点:
远程调用和本地调用的处理逻辑是不同的, 正确的做法应该是把远程调用通过一个中间层转化为本地调用, 如果把两者两者混为一谈, 后期可能会出现无法区分业务的情况比如对于组件无法响应的问题, 远程调用可能直接显示一个 404 页面, 但是本地调用可能需要做其他处理如果不加以区分, 那么久无法完成这种业务要求
远程调用只能传能被序列化为 json 的数据, 像 UIImage 这样非常规的对象是不行的所以如果组件接口要考虑远程调用, 这里的参数就不能是这类非常规对象, 接口的定义就受限了出现这种情况的原因就是, 远程调用是本地调用的子集, 这里混在一起导致组件只能提供子集功能(远程调用), 所以这个方案是天生有缺陷的
理论上来讲, 组件化是接口层面的东西, 应该用语言自身的特性去解决, 而 url 是用于远程通信的, 不应该和组件化扯上关系
改进
针对上述第二点描述的无法传递常规对象的问题, 蘑菇街做了改进, 通过 protocol 转 class 的方式去实现, 但是我想说这种实现办法真是越高越复杂了具体看代码就知道了
protocolMediator 实现:
功能: 通过 protocol 的字符串存储 class
- #import <Foundation/Foundation.h>
- @interface ProtocolMediator : NSObject
- + (instancetype)sharedInstance;
- - (void)registerProtocol:(Protocol *)proto forClass:(Class)cls;
- - (Class)classForProtocol:(Protocol *)proto;
- @end
- ============
- #import "ProtocolMediator.h"
- @interface ProtocolMediator()
- @property (nonatomic,strong) NSMutableDictionary *protocolCache;
- @end
- @implementation ProtocolMediator
- + (instancetype)sharedInstance
- {
- static ProtocolMediator *mediator;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- mediator = [[ProtocolMediator alloc] init];
- });
- return mediator;
- }
- -(NSMutableDictionary *)protocolCache{
- if (!_protocolCache) {
- _protocolCache = [NSMutableDictionary new];
- }
- return _protocolCache;
- }
- - (void)registerProtocol:(Protocol *)proto forClass:(Class)cls {
- [self.protocolCache setObject:cls forKey:NSStringFromProtocol(proto)];
- }
- - (Class)classForProtocol:(Protocol *)proto {
- return self.protocolCache[NSStringFromProtocol(proto)];
- }
- @end
commonProtocol 实现:
功能: 所有需要传递非常规参数的方法都放在这里定义, 然后各个组件自己去具体实现(这里为了演示方便, 使用的常规的字符串和 int 类型当然也可以传递 UIImage 等非常规对象)
- #import <Foundation/Foundation.h>
- @protocol A_VC_Protocol <NSObject>
- -(void)action_A:(NSString*)para1;
- @end
- @protocol B_VC_Protocol <NSObject>
- -(void)action_B:(NSString*)para para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4;
- @end
组件 A 实现:
- #import <UIKit/UIKit.h>
- #import "CommonProtocol.h"
- @interface A_VC : UIViewController<A_VC_Protocol>
- @end
- =============================
- #import "A_VC.h"
- #import "ProtocolMediator.h"
- @implementation A_VC
- // 注册自己的 class
- +(void)load{
- [[ProtocolMediator sharedInstance] registerProtocol:@protocol(A_VC_Protocol) forClass:[self class]];
- }
- // 调用组件 B, 先通过 protocol 字符串取出类 class, 然后再实例化之调用组件 B 的方法
- -(void)btn_click{
- Class cls = [[ProtocolMediator sharedInstance] classForProtocol:@protocol(B_VC_Protocol)];
- UIViewController<B_VC_Protocol> *B_VC = [[cls alloc] init];
- [B_VC action_B:@"param1" para2:222 para3:333 para4:444];
- }
- -(void)action_A:(NSString*)para1 {
- NSLog(@"call action_A: %@",para1);
- }
- @end
组件 B 实现
- #import <UIKit/UIKit.h>
- #import "CommonProtocol.h"
- @interface B_VC : UIViewController<B_VC_Protocol>
- @end
- =============
- #import "B_VC.h"
- #import "ProtocolMediator.h"
- @implementation B_VC
- +(void)load{
- [[ProtocolMediator sharedInstance] registerProtocol:@protocol(B_VC_Protocol) forClass:[self class]];
- }
- -(void)btn_click{
- Class cls = [[ProtocolMediator sharedInstance] classForProtocol:@protocol(A_VC_Protocol)];
- UIViewController<A_VC_Protocol> *A_VC = [[cls alloc] init];
- [A_VC action_A:@"param1"];
- }
- -(void)action_B:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
- NSLog(@"call action_B: %@---%zd---%zd---%zd",para1,para2,para3,para4);
- }
- @end
原理和缺点
每个组件先通过 Mediator 拿到其他的组件对象 class, 然后在实例化该 class 为实例对象, 再通过该对象去调用它自身实现的 protocol 方法, 因为是通过接口的形式实现的方法, 所以任何类型参数都是可以传递的
但是这会导致一个问题: 组件方法的调用是分散在各地的, 没有统一的入口, 也就没法做组件不存在时的统一处理
从上面的实现就可以看出来 A 调用 B 不是直接通过 mediator 去调用, 而是先通过 mediator 生成其他组件的对象, 然后自己再用该对象去调用其他组件的方法, 这就导致组件方法调用分散在各个调用组件内部, 而不能像 target-action 方案那样对所有组件的方法调用进行统一的管理
再者这种方式让组件同时依赖两个中心: ProtocolMediator 和 CommonProtocol, 依赖越多, 后期扩展和迁移也会相对困难
并且这种调用其他组件的方式有点诡异, 不是正常的使用方法, 一般都是直接你发起一个调用请求, 其他组件直接把执行结果告诉你, 但是这里确实给你返回一个组件对象, 让你自己在用这个对象去发起请求, 这操作有点蛋疼
总结
其实蘑菇街的 url-scheme 加上 protocol-class 方案一起提供组件间跳转和调用会让人无所适从, 使用者还要区分不同的参数要使用的不同的方法, 而 target-action 方案可以用相同的方法来传递任意参数综上所述, target-action 方案更优
Demo 下载
- url-scheme
- protocol-class
- target-action
组件化方案实施
来源: https://juejin.im/post/5a7dcc025188254e5c6c6120