iOS 一个轻量级的组件化思路

前言

提及组件化你们应该都不陌生,不过也再提一下,因为业务的复杂度扩展,各个模块之间的耦合度愈来愈高,不但形成了“牵一发动全身”的尴尬境地,还增长了测试的重复工程,此时,组件化就值得考虑了。组件化就是将APP拆分红各个组件(或者说模块),同时解除这些组件之间的耦合,而后经过路由中间件将项目所须要的组件结合起来。这样作的好处有:html

  • 解耦合,加强可移植性,不用再自身业务模块中大量引入其余业务的头文件。
  • 提升复用性,若是其余项目中有相似的功能,直接将模块引入稍做修改就能使用了。
  • 减小测试成本,当修改或者迭代某个小组件的过程当中就不用进行大规模的回归测试。

网上关于组件化的方案很多,流传最广的是蘑菇街组件化的技术方案iOS应用架构谈 组件化方案这里不对大佬们的方案妄加评价,感兴趣的同窗能够本身看看。这里咱们聊聊另外的一种方式Protocol-Moudlegit

思路

在iOS中,协议(Protocol)定义了一个纲领性的接口,全部类均可以选择实现。它主要是用来定义一套对象之间的通讯规则。protocol也是咱们设计时经常使用的一个东西,相对于直接继承的方式,protocol则偏向于组合模式。他使得两个绝不相关的类可以相互通讯,从而实现特定的目标。github

在以前的一篇文章ResponderChain+Strategy+MVVM实现一个优雅的TableView中咱们用到了protocol来为View提供公共的方法:bash

- (void)configCellDateByModel:(id<QFModelProtocol>)model;架构

为Model提供公共的方法:ide

- (NSString *)identifier;
- (CGFloat)height;
复制代码

那么咱们也能够以此来构建一个轻量级的路由中间件,定义一套各个组件的通讯规则,各自管理和维护各自的模块,对外提供必要的接口。组件化

实践

首先看一下这个Demo的结构图和运行效果
post

结构

效果

路由

好了咱们看看路由的一些细节,它只须要提供两个关键的东西:测试

  1. 提供路由器单例
  2. 获取对应的Moudle
    • 经过Protocol获取
    • 经过URL获取

首先提供单例:ui

+ (instancetype)router {
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _router = [[self alloc]init];
    });
    return _router;
}
复制代码

这样的单例可能没有人不会写,but,这其实这仅仅是个“假的”单例,不信你可使用[QFRouter router][[QFRouter alloc]init]以及[router copy]试试他们会不会生成三个内存地址不一样的实例。可能你会说,谁会无聊这么干?可是若是设计的时候能更严谨的规避这种坑不会更好么。 那么怎么才能才能作一个严谨的单例呢?能够重写他的alloccopy方法避免建立多个实例,你能够在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私有库,而须要多个团队知道的也就只有这个公共协议库,把全部的协议放入这个公开协议库中,每次升级也只须要更新这个库就行了。

关于对组件化的态度:上面说了那么多好处,下面说说弊端(亲身体会)
若是团队规模较小业务复杂度较低的话,不太建议作私有库,缘由:

  1. 它的 修改 -> 升级 -> 集成 -> 测试是一个比较耗时的过程,可能一点小小的改动(好比改个颜色,换个背景)须要耗费成倍的时间。
  2. 增长项目的调用复杂度,增长新成员的熟悉成本。

任何事情都是双面的,有利有弊,望各位看官自行取舍。最后因为笔者文笔有限,若给您形成了困扰,实在抱歉,有任何疑问or意见or建议,欢迎交流讨论,固然,若是对您有用,但愿顺手给个Star,点个关注。赠人玫瑰,手留余香,您的支持是我最大的动力!

相关文章
相关标签/搜索