为什么要进行组件化?

项目开发初期,一般都是单项目工程,一般是一个主工程加一些第三方库,APP一般有多个模块,模块之间会进行通信和互相调用。

我们以商城类APP为例,一般有商品模块,购物车模块,订单模块,商品需要调用购物车模块,执行加入购物车操作,购物车也可以跳转到商品模块,同时商品也可以调用订单模块直接下单,订单也可以跳转到商品模块等等,我们通常的做法哪个模块用到了其它模块的东西,直接 import 对应的文件即可。

这样直接导入简单明了,初期方便快速开发,但是随着项目越来越庞大,就会出现一些问题,模块之间耦合严重,编译花费时间更长,扩展不方便等。根据软件工程的思想,可以增加一个中间层来处理这个问题。

各个模块都通过中间层跟其它模块通信,中间层要解决的问题有如下几个:

  1. 中间层怎么转发组件间的调用?
  2. 一个模块只跟中间层通信,如何方便发现其他模块提供什么接口?
  3. 模块依赖中间层,中间层依赖所有模块,如何破除这层相互依赖关系?

CTMediator方案原理

前两个问题最直接的方法就是在 Mediator 里面直接提供对应模块的方法。比如从订单模块跳转到商品模块

//Mediator.m 中间件
#import "ProductDetailComponent.h"

@implementation Mediator
// 商品模块的接口
+ (UIViewController *)ProductDetailComponent_viewController:(NSString *)productId {
// 直接调用商品组件的接口
return [ProductDetailComponent detailViewController:productId];
}
// ProductDetailComponen.m
// ProductDetailComponent 组件
#import "ProductDetailViewController.h"

@implementation ProductDetailComponent
// 商品模块对外接口
+ (UIViewController *)detailViewController:(NSString *)productId {
ProductDetailViewController *detailVC = [[ProductDetailViewController alloc] initWithProductId:productId];
return detailVC;
}
@end
//订单模块调用商品模块
#import "Mediator.h"

@implementation OrderViewController
- (void)gotoProductDetail:(NSString *)productId {
UIViewController *detailVC = [Mediator ProductDetailComponent_viewController:productId];
[self.navigationController pushViewController:detailVC];
}
@end

这样就实现了 Mediator 中间层,但是这样处理,Mediator 依赖了所有模块,调用者又依赖 Mediator,相互依赖关系并没有得到解决,那要怎么处理呢?

我们可以利用 OC 的 runtime 机制来解决,组件依赖 Mediator,Mediator中不在直接调用组件的方法,而是通过 runtime 反射调用。

//Mediator.m 中间件使用 runtime 查找组件方法

@implementation Mediator
// 商品模块的接口
+ (UIViewController *)ProductDetailComponent_viewController:(NSString *)productId {
// runtime 查找组件方法
Class cls = NSClassFromString(@"ProductDetailComponen");
return [cls performSelector:NSSelectorFromString(@"detailViewController:") withObject:@{@"productId":productId}];
}

这样 Mediator 就不用 import 组件了,架构图变成了下面这种。

这种方式我们还可以进一步优化:

  1. Mediator 通过 runtime 查找组件方法可以抽取出来。
  2. Mediator 文件中要封装所有组件的对外方法,组件一多,Mediator 类的长度将无比恐怖。

casa 的方案就对上面两点进行了优化,target-action 对应第一点,target 就是class,action 就是 selector,通过一些规则简化动态调用。Category 对应第二点,每个组件写一个 Mediator 的 Category,让 Mediator 类文件不至于太长。

如图所示,组件提供 Target-Action 对外接口,CTMediator 通过 runtime 调用Target-Action 解耦,组件的 Category 分离组件接口代码,让 Mediator 类文件不至于过长。

组件化的实施

业务分层

实施组件化的第一步是进行组件分层,这个分层是抽象意义上的,我们一般分为三层:

  • 业务层:具体的业务模块,比如首页模块,商城模块等。
  • 通用层:包括一些通用的UI控件,网络等,为业务层提供业务支持。
  • 基础层:包括自己开发的基础库和第三方库,一般是可以进行独立发版的,其它项目也可以使用的库。

在分层架构中,只能上层对下层依赖,下层不能对上层有依赖,下层中不要包含上层业务逻辑

CTMediator的实施

casa 提供了详细的实施步骤,具体可以看在现有工程中实施基于CTMediator的组件化方案一文,我这里就不再赘述。

组件化中的一些场景处理

1. 公用图片资源在组件中如何处理?
答:单独创建一个子工程,里面只有图片。然后在其它使用图片的子工程里,只把这个图片子工程写入 podfile,而不写入 podspec 的 dependency 里面,这样能确保调试的时候有图片,组件发版的时候不带图片。然后在主工程的 podfile 里面同样也写入图片子工程,这样就好了。

2. 之前很多类都继承了一个基类,要怎么处理?
答:把基类拿出来做成一个 Pod,然后使用到的组件依赖此基类 Pod。

3. 什么情况下需要做组件化?模块之间是如何划分的?
答:组件化更多的是针对横向依赖做依赖下沉。业务相对于服务之间的依赖属于纵向依赖,把服务作为普通pod引入即可。业务和业务之间是横向依赖,必须组件化。

4. 组件的开发人员是如何开发的?

  • 绝大多数情况下只工作在组件工程里。当组件需要调度另外一个组件的时候,这个组件会写在Podfile里,不会写在podspec里。所以跑完整场景的时候,是不需要完整运行一个App的。
  • 只要保证自己的组件工程没问题,那就没问题了。如果要联调,那就只需要在组件工程的Podfile里把别的组件引入就好。同1
  • 组件工程里除了组件本身,Target之外。还有组件自身的测试代码,例如启动页(往往就是一个tableview给到各功能、各场景的入口)、功能调度代码等用于组件自己的测试。这些测试代码是不会跟随组件的发布流程的(podspec文件里面可以指定需要跟随发版的文件路径)。
  • 总之,这个组件是可以独立作为一个App跑在真机上的,可以通过启动页的入口,或者代码编写的测试入口点进去看。例如我们的支付组件,它的首页就包含了各种支付方式的入口,点进去就直接生成伪订单并采用这种支付方式来做测试,而不必在主工程中去跑完整流程。

5. 请问 shouldCacheTarget 的参数是否有必要暴露在 performTarget 的接口中?比如说一般什么时候应该传 YES,什么时候应该传 NO 呢?

在传no的情况下,target对象在初始化之后,跑完action就立刻释放了。如果action中调用了异步方法,这种情况就会导致接收不到异步方法的回调。

所以当你要调用的target的这个action含有异步方法的时候,此时应该传YES。当异步回调结束你拿到异步回调的时候再把这个target给clean一下。

比如你要上传文件,你通过category传了文件地址、成功回调block、失败回调block这三个参数给target-action。target-action就会调用异步上传接口进行上传。上传完毕之后弹框,然后调用你之前传过来的block。

这种情况target-action需要知道上传组件上传完毕,它才能弹框,才能调用你给的block。如果shouldCacheTarget不是yes,action跑完上传接口,就直接退出了,target直接回收了。它就永远没有机会弹窗,也没有机会调用你给的block了