iOS组件化实践
前述
- 目前业界主要两种方式:URL/protocol 注册调度、runtime 调度
- 此处我采用的 runtime 调度法,参考Casa Taloyum 组件化方案
- 以 GitHub 中搭建一个新项目为例,之前在公司是用公司搭的gitlab服务器;
- 在实践后的一些想法,一个项目如果有几十人维护多个子项目时,采用私有仓库源,同时维护多个子项目,是非常好的团队管理方式; 但是如果一个团队人数不多,我觉得就会为团队成员带来一些维护私有仓库的成本,降低了灵活性;因此我在公司时采用了中间件的方案,但是没有采用管理私有仓库源;
- CRMediator 已发布到公有仓库
- 此处实践是采用私有库组件化管理
结果
准备工作
- 在 GitHub 页面右上角点击
+然后 New organization`,创建一个 organization 存放源仓库和组件 - 创建一个 GitHub repo,这个 repo 作为私有 podspecs,可参考CocoaPods公有/私有库管理
- 创立一个文件夹,例如 Project,把我们的主工程文件夹放到 Project 下,后面的组件和中间件都放在该文件夹
实现效果描述
MainProject是一个非常简单的应用,一共就三个页面。首页 push 到 AViewController,AViewController push 到 BViewController。我们可以理解成这个工程由三个业务组成:首页、A业务、B业务。
我们需要为此做四个私有Pod: * A业务Pod(以后简称A Pod) * 方便其他人调用A业务的CRMediator category的Pod(以后简称A_Category Pod) * A_Category Pod本质上只是一个方便方法,它对A Pod不存在任何依赖 * B业务Pod(以后简称B Pod) * 方便其他人调用B业务的CRMediator category的Pod(以后简称B_Category Pod)
实践过程
第一步 创建B业务Pod
生成的目录结构如下
│ ├── B
│ │ ├── BViewController.h
│ │ ├── BViewController.m
│ │ └── Targets
│ │ ├── Target_B.h
│ │ └── Target_B.m
BViewController 为 B 业务的展示视图,只在中间显示上个页面跳转过来的文字。 Target_B 为将要暴露给 B_Category 调用的方法 代码如下:
头文件:
@interface Target_B : NSObject
- (UIViewController *)Action_viewController:(NSDictionary *)params;
@end
实现文件:
@implementation Target_B
- (UIViewController *)Action_viewController:(NSDictionary *)params {
BViewController *vc = [[BViewController alloc] initWithText:params[@"text"]];
return vc;
}
@end
因为Target对象处于B的命名域中,所以Target对象中可以随意import B业务线中的任何头文件。
另外补充一点,Target对象的Action设计出来也不是仅仅用于返回ViewController实例的,它可以用来执行各种属于业务线本身的任务。例如上传文件,转码等等各种任务其实都可以作为一个Action来给外部调用,Action完成这些任务的时候,业务逻辑是可以写在Action方法里面的。
换个角度说就是:Action具备调度业务线提供的任何对象和方法来完成自己的任务的能力。它的本质就是对外业务的一层服务化封装。
现在我们这个Action要完成的任务只是实例化一个ViewController并返回出去而已,根据上面的描述,Action可以完成的任务其实可以更加复杂。
第二步 创建B_Category Pod
B_Category 只创建了 CRMediator+B,用于调用 B Pod 中的业务实现
│ ├── B_Category
│ │ ├── CRMediator+B.h
│ │ └── CRMediator+B.m
CRMediator+B 代码如下:
头文件:
@interface CRMediator (B)
/**
返回B业务视图实例
@param text 显示文字
@return return value description
*/
- (UIViewController *)bViewControllerWithText:(NSString *)text;
@end
实现文件:
@implementation CRMediator (B)
- (UIViewController *)bViewControllerWithText:(NSString *)text {
return [self performWithTargetName:@"B" actionName:@"viewController" params:@{@"text": text}];
}
@end
performWithTargetName:@“B”中给到的@“B”其实是Target对象的名字。一般来说,一个业务Pod只需要有一个Target就够了,但一个Target下可以有很多个Action。Action的名字也是可以随意命名的,只要到时候Target对象中能够给到对应的Action就可以了。
这样写好后,在别的组件引用调用写法如下:
UIViewController *vc = [[CRMediator shareInstance] bViewControllerWithText:@"文字"];
[self.navigationController pushViewController:vc animated:YES];
将B Pod 和 B_Category Pod 提交到私有仓库
第三步 创建A业务Pod
在 A 的 Podfile 中添加 B_Category 引用
source 'https://github.com/cr-atomic/CRPrivateRepos.git'
pod 'B_Category', :git => 'https://github.com/cr-atomic/B_Category.git'
此时A业务就可以调用B业务的实现,在AViewController中添加调用B业务代码
- (void)buttonAction:(UIButton *)button {
UIViewController *bVC = [[CRMediator shareInstance] bViewControllerWithText:@"A进入到B显示文字"];
if (self.navigationController) {
[self.navigationController pushViewController:bVC animated:YES];
} else {
[self presentViewController:bVC animated:YES completion:nil];
}
}
添加一个 Target_A,类似 Target_B,提交代码到私有仓库
第四步 创建A_Category Pod
类似B_Category Pod创建方式
在 MainProject 中调用
在 MainProject 的 Podfile 中添加组件和中间件引用
pod 'A', :git => 'https://github.com/cr-atomic/A.git'
pod 'A_Category', :git => 'https://github.com/cr-atomic/A_Category.git'
pod 'B', :git => 'https://github.com/cr-atomic/B.git'
此时我们就可以在首页调用A业务的方法
UIViewController *vc = [[CRMediator shareInstance] aViewControllerWithText:@"MainProject 进入显示文字"];
[self.navigationController pushViewController:vc animated:YES];
总结
hard code
这个组件化方案的hard code仅存在于Target对象和Category方法中,影响面极小,并不会泄漏到主工程的业务代码中,也不会泄漏到业务线的业务代码中。
而且在实际组件化的实施中,也是依据category去做业务线的组件化的。所以先写category里的target名字,action名字,param参数,到后面在业务线组件中创建Target的时候,照着category里面已经写好的内容直接copy到Target对象中就肯定不会出错(仅Target对象,并不会牵扯到业务线本身原有的对象)。
如果要消除这一层hard code,那么势必就要引入一个第三方pod,然后target对象所在的业务线和category都要依赖这个pod。为了消除这种影响面极小的hard code,而且只要按照章法来就不会出错。为此引入一个新的依赖,其实是不划算的。
命名域问题
在这个实践中,响应者的命名域并没有泄漏到除了响应者以外的任何地方,这就带来一个好处,迁移非常方便。
比如我们的响应者是一个上传组件。这个上传组件如果要替换的话,只需要在它外面包一个Target-Action,就可以直接拿来用了。而且包Target-Action的过程中,不会产生任何侵入性的影响。
例如原来是你自己基于AFNetworking写的上传组件,现在用了七牛SDK上传,那么整个过程你只需要提供一个Target-Action封装一下七牛的上传操作即可。不需要改动七牛SDK的代码,也不需要改动调用方的代码。倘若是基于URL注册的调度,做这个事情就很蛋疼。
服务管理问题
由于Target对象处于响应者的命名域中,Target对象就可以对外提供除了页面实例以外的各种Action。
而且,由于其本质就是针对响应者对外业务逻辑的Action化封装(其实就是服务化封装),这就能够使得一个响应者对外提供了哪些Action(服务),Action(服务)的实现逻辑是什么得到了非常好的管理,能够大大降低将来工程的维护成本。然后Category解决了服务应该怎么调用的问题。
但在基于URL注册机制和Protocol共享机制的组件化方案中,由于服务散落在响应者各处,服务管理就显得十分困难。如果还是执念于这样的方案,大家只要拿上面提到的三个问题,对照着URL注册机制和Protocol共享机制的组件化方案比对一下,就能明白了。
另外,如果这种方案把所有的服务归拢到一个对象中来达到方便管理的目的的话,其本质就已经变成了Target-Action模式,Protocol共享机制其实就已经没有存在意义了。
侵入性问题
正如你所见,CRMediator组件化方案的实施非常安全。因为它并不存在任何侵入性的代码修改。
对于响应者来说,什么代码都不用改,只需要包一层Target-Action即可。例如本例中的B业务线作为A业务的响应者时,不需要修改B业务的任何代码。
对于调用者来说,只需要把调用方式换成CRMediator调用即可,其改动也不涉及原有的业务逻辑,所以是十分安全的。
另外一个非侵入性的特征体现在,基于CRMediator的组件化方案是可以循序渐进地实施的。这个方案的实施并不要求所有业务线都要被独立出来成为组件,实施过程也并不会修改未组件化的业务的代码。
在独立A业务线的过程中如果涉及其它业务线(B业务线)的调用,就只需要给到Target对象即可,Target对象本身并不会对未组件化的业务线(B业务线)产生任何的修改。而且将来如果对应业务线需要被独立出去的时候,也仅需要把Target对象一起复制过去就可以了。
但在基于URL注册和protocol共享的组件化方案中,都必须要在未组件化的业务线中写入注册代码和protocol声明,并分配对应的URL和protocol到具体的业务对象上。这些其实都是不必要的,无端多出了额外维护成本。
注册问题
CTMediator没有任何注册逻辑的代码,避免了注册文件的维护和管理。Category给到的方法很明确地告知了调用者应该如何调用。
例如B_Category给到的- (UIViewController *)bViewControllerWithText:(NSString *)text;方法。这能够让工程师一眼就能够明白使用方式,而不必抓瞎拿着URL再去翻文档。
这可以很大程度提高工作效率,同时降低维护成本。