内容简介:说起组件化大家应该都不陌生,不过也再提一下,由于业务的复杂度扩展,各个模块之间的耦合度越来越高,不但造成了“牵一发动全身”的尴尬境地,还增加了测试的重复工程,此时,组件化就值得考虑了。组件化就是将APP拆分成各个组件(或者说模块),同时解除这些组件之间的耦合,然后通过路由中间件将项目所需要的组件结合起来。这样做的好处有:网上关于组件化的方案不少,流传最广的是在iOS中,协议(Protocol)
说起组件化大家应该都不陌生,不过也再提一下,由于业务的复杂度扩展,各个模块之间的耦合度越来越高,不但造成了“牵一发动全身”的尴尬境地,还增加了测试的重复工程,此时,组件化就值得考虑了。组件化就是将APP拆分成各个组件(或者说模块),同时解除这些组件之间的耦合,然后通过路由中间件将项目所需要的组件结合起来。这样做的好处有:
- 解耦合,增强可移植性,不用再自身业务模块中大量引入其他业务的头文件。
- 提高复用性,如果其他项目中有类似的功能,直接将模块引入稍作修改就能使用了。
- 减少测试成本,当修改或者迭代某个小组件的过程中就不用进行大规模的回归测试。
网上关于组件化的方案不少,流传最广的是 蘑菇街组件化的技术方案 和 iOS应用架构谈 组件化方案 这里不对大佬们的方案妄加评价,感兴趣的同学可以自己看看。这里我们聊聊另外的一种方式 Protocol-Moudle
思路
在iOS中,协议(Protocol) 定义了一个纲领性的接口,所有类都可以选择实现。它主要是用来定义一套对象之间的通信规则。protocol也是我们设计时常用的一个东西,相对于直接继承的方式,protocol则偏向于组合模式。他使得两个毫不相关的类能够相互通信,从而实现特定的目标。
在之前的一篇文章 ResponderChain+Strategy+MVVM实现一个优雅的TableView 中我们用到了protocol来为View提供公共的方法:
- (void)configCellDateByModel:(id<QFModelProtocol>)model;
为Model提供公共的方法:
- (NSString *)identifier; - (CGFloat)height; 复制代码
那么我们也可以以此来构建一个轻量级的路由中间件,定义一套各个组件的通信规则,各自管理和维护各自的模块,对外提供必要的接口。
实践
首先看一下这个Demo的结构图和运行效果
路由
好了我们看看路由的一些细节,它只需要提供两个关键的东西:
- 提供路由器单例
- 获取对应的Moudle
- 通过Protocol获取
- 通过URL获取
首先提供单例:
+ (instancetype)router { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _router = [[self alloc]init]; }); return _router; } 复制代码
这样的单例可能没有人不会写,but,这其实这仅仅是个“假的”单例,不信你可以使用 [QFRouter router]
和 [[QFRouter alloc]init]
以及 [router copy]
试试他们会不会生成三个内存地址不同的实例。可能你会说,谁会无聊这么干?但是如果设计的时候能更严谨的规避这种坑不会更好么。 那么怎么才能才能做一个严谨的单例呢?可以重写他的 alloc
和 copy
方法避免创建多个实例,你可以在 Demo工程 中看到细节,这里不做展开。
回归正题,我们看看如何获取Module
通过Runtime的反射机制,我们可以通过NSString获取一个class进而创建对应的对象,而Protocol又可以得到一个NSString,那么是否可以由此入手呢?答案是可以的:
- (Class)classForProtocol:(Protocol *)protocol { NSString *classString = NSStringFromProtocol(protocol); return NSClassFromString(classString); } 复制代码
这里传入一个protocol即可获取对应的Module的class,再通过class即可以得到对应的Module的object。
通过Protocol或者URL获取对应的Module:
#pragma mark - Public - (id)interfaceForProtocol:(Protocol *)protocol { Class class = [self classForProtocol:protocol]; return [[class alloc]init]; } - (id)interfaceForURL:(NSURL *)url { id result = [self interfaceForProtocol:objc_getProtocol(url.scheme.UTF8String)]; NSURLComponents *cp = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; [cp.queryItems enumerateObjectsUsingBlock:^(NSURLQueryItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { [result setValue:obj.value forKey:obj.name];//KVC设置 }]; return result; } 复制代码
这里有个瑕疵就是调用方(外部组件)需要知道这个目标组件对外暴露的协议名称,不过既然是协议,对外公开应该不算大问题吧,调用实例如下:
wayOne:
id <MoudleHome>homeMoudle = [[QFRouter router]interfaceForProtocol:@protocol(MoudleHome)]; 复制代码
这样就拿到了对应的目标组件实例,通过对外暴露的属性可以对其进行传值,通过其回调block则可以拿到回调参数。
wayTwo:
id <MoudleMe>meMoudle = [[QFRouter router]interfaceForURL:[NSURL URLWithString:@"MoudleMe://?paramterForMe=ModuleMe"]]; 复制代码
这里通过url传入,通过KVC设置其属性值。同样地,通过其回调block则可以拿到回调参数。
公共协议
通过上面我们了解到:通过Protocol可以获取对应的组件实例,那么这个协议放在哪儿?如何管理呢?
在日常开发过程中,跨组件的交互场景最多的应该就是:从组件A附带参数跳转到组件B的某个页面,组件B的这个页面中做一些操作,再回到组件A(可能有回调参数,也可能不回调参数),那么我们的协议应该能处理这两个最常见和基础的操作,所以给protocol定义了两个属性:
typedef void(^QFCallBackBlock)(id parameter); #pragma mark - 基础协议 @protocol QFMoudleProtocol <NSObject> /// 暴露给组件外部的控制器,一般为该组件的主控制器 @property (nonatomic, weak) UIViewController *interfaceViewController; /// 回调参数 @property (nonatomic, copy) QFCallBackBlock callbackBlock; @end 复制代码
这里的interfaceViewController为何声明成了weak属性?这个问题先留一下,后面会聊到这一点。
有了这里的两个属性我们即可完成,对应的跳转和参数回调,但是如何正向传值呢?
应该还需要对应的属性来做入参,但是组件何其多,入参何其多,如果都把正向的属性写入这里面,那么随着时间和业务的增长,这个协议可能会十分杂乱和臃肿。
所以这里把这个协议定为基础协议,对应的组件都继承自它,然后定义各自的需要的入参属性:
首页组件:
#pragma mark - ”首页“组件 @protocol MoudleHome <QFMoudleProtocol> /// 组件“Home”首页所需要的参数 @property (nonatomic, copy) NSString *paramterForHome; /// 组件“Home”中详情页面所需要的参数 @property (nonatomic, copy) NSString *titleString; /// 组件“Home”中详情页面所需要的参数 @property (nonatomic, copy) NSString *descString; /// 组件“Home”所需要暴露的特殊接口,比如其他组件也要跳转到该页面 @property (nonatomic, weak) UIViewController *detailViewController; @end 复制代码
可以看到,由于首页组件需要对外暴露一个主页面 QFHomeViewController
和详细页面 QFDetailViewController
所以参数会多一点。
我的组件:
#pragma mark - “我的”组件 @protocol MoudleMe <QFMoudleProtocol> /// 组件“Me”所需要的参数 @property (nonatomic, copy) NSString *paramterForMe; @end 复制代码
而“我的”组件,只对外提供一个 QFMeViewController
页面,参数比较简单。
这样,基本算是达成了对协议的处理,但是无可避免的问题就是: 这个公共协议中定义了各个组件的协议,所以需要对多个开发团队可见,感觉这也是组件化过程中的一个普遍问题,目前没找到好的解决方式。
Module
上面我们说到了公开protocol中定义了一些属性,比如 interfaceViewController
那么这些属性由谁提供呢?没错,就是Module,通过上面的步骤我们可以获取到对应的Module实例,但是我们跳转需要的是Controller,所以,在此时就需要Module的帮助了, Module通过公共协议定义的属性为外部提供Controller接口:
- (UIViewController *)interfaceViewController { QFHomeViewController *homeViewController = [[QFHomeViewController alloc]init]; homeViewController.interface = self; interfaceViewController = (UIViewController *)homeViewController; return interfaceViewController; } 复制代码
因为Module是在对应的组件中,所以可以随意引用自己组件内部的头文件完成初始化,
而对应的控制器中,需要组件外部的参数,所以这里把Module实例也暴露给对应的控制器实例,也就是 homeViewController.interface = self;
所做的事情。
在上面说协议的时候我们提到为什么要使用weak,至此,应该比较明朗了 ———— 打破循环强引用。
通过公共协议解耦获取到Module,Module完成为组件内和组件外的搭桥铺路工作,由此,使得跨组件传值、调用、参数回调得以实现。更多细节请看 QFMoudle 。
由于时间关系,如何制作私有库就不再赘述了,有需要欢迎留言,我们一起手把手创建一个属于你自己的pod私有库。
后记
在这种组件化的实践中,一般会把对应的组件、路由以及公共协议做成pod私有库,而需要多个团队知道的也就只有这个公共协议库,把所有的协议放入这个公开协议库中,每次升级也只需要更新这个库就好了。
关于对组件化的态度:上面说了那么多好处,下面说说弊端(亲身体会)
如果团队规模较小业务复杂度较低的话,不太建议做私有库,原因:
- 它的 修改 -> 升级 -> 集成 -> 测试是一个比较耗时的过程,可能一点小小的改动(比如改个颜色,换个背景)需要耗费成倍的时间。
- 增加项目的调用复杂度,增加新成员的熟悉成本。
任何事情都是双面的,有利有弊,望各位看官自行取舍。最后由于笔者文笔有限,若给您造成了困扰,实在抱歉,有任何疑问or意见or建议,欢迎交流讨论,当然,如果对您有用,希望顺手给个Star,点个关注。赠人玫瑰,手留余香,您的支持是我最大的动力!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Effective Engineer
Edmond Lau / The Effective Bookshelf, Palo Alto, CA. / 2015-3-19 / USD 39.00
Introducing The Effective Engineer — the only book designed specifically for today's software engineers, based on extensive interviews with engineering leaders at top tech companies, and packed with h......一起来看看 《The Effective Engineer》 这本书的介绍吧!