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

前言

说起组件化大家应该都不陌生,不过也再提一下,由于业务的复杂度扩展,各个模块之间的耦合度越来越高,不但造成了“牵一发动全身”的尴尬境地,还增加了测试的重复工程,此时,组件化就值得考虑了。组件化就是将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的结构图和运行效果

结构

效果

路由

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

  1. 提供路由器单例
  2. 获取对应的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]试试他们会不会生成三个内存地址不同的实例。可能你会说,谁会无聊这么干?但是如果设计的时候能更严谨的规避这种坑不会更好么。 那么怎么才能才能做一个严谨的单例呢?可以重写他的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,点个关注,您的支持是我最大的动力!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容