一、中小型App为什么要组件化
当项目App处于起步阶段、各个需求模块趋于成熟稳定的过程中,组件化也许并没有那么迫切,甚至考虑组件化的架构可能会影响开发效率和需求迭代。而当项目迭代到一定时期之后,便会出现一些相对独立的业务功能模块,而团队的规模也会随着项目迭代逐渐增长,这便是中小型应用考虑组件化的时机了。
为了更好的分工协作,团队会安排团队成员各自维护一个相对独立的业务组件。这个时候我们引入组件化方案,一是为了解除组件之间相互引用的代码硬依赖,二是为了规范组件之间的通信接口; 让各个组件对外都提供一个黑盒服务,而组件工程本身可以独立开发测试,减少沟通和维护成本,提高效率。
进一步发展,当团队涉及到转型或者有了新的立项之后,一个团队会开始维护多个项目App,而多个项目App的需求模块往往存在一定的交叉,而这个时候组件化给我们的帮助会更大,我只需要将之前的多个业务组件模块在新的主App中进行组装即可快速迭代出下一个全新App。
二、如何开始组件化工作
2.1 组件化的架构目标
在详细说如何具体开始组件化工作之前,我们对于组件化的期望应该是这样的,一个团队维护一到两个独立App,每个独立App除开包含一些产品相关的非独立模块集之外,还需要用一些独立的业务组件进行组装。 而不管是产品的非独立模块集、还是独立业务组件都需要底层公共库和基础库的支持。如下图所示:
2.2 组件化第一步-剥离公共库和产品基础库
在具体的项目开发过程中,我们使用cocoapod的组件依赖管理利器已经开始从Github上引入了一些第三方开源的基础库,比如说AFNetworking、SDWebImage、SVProgressHUD、ZipArchive等。除开这些第三方开源基础库之外,我们还需要做的事情就是将一些基础组件从主工程剥离出来,形成产品自己的私有基础库仓库,为我们进行业务独立组件的分离做准备。
这部分我将其分为两类:一类是公共基础库,用于跨产品使用;一类是产品基础库,在某个产品中强相关依赖使用。这里以我们自己产品划分为例,概述一下这两类库都包括哪些基础组件:
公共库包括:组件化中间件、网络诊断、第三方SDK管理封装、长连接相关、Patch相关、网络和页面监控相关、用户行为统计库、第三方分享库、JSBridge相关、关于Device+file+crypt+http的基础方法等。
产品基础库包括:通用的WebViewContainer组件(封装了JSBridge)、自定义数字键盘、表情键盘、自定义下拉列表、循环滚动页面、AFNeworking封装库(对上层业务隐藏AF的直接引用)、以及其他自定义的UI基础组件库。
2.2 组件化第二步-独立业务模块单独成库
在基础库成体系的基础上,我们就可以开始按照需求定性将一些相对独立的业务模块独立成库,单独在一个工程上进行开发、测试。
往往在这个阶段有一个误区,千万不能为了组件化而强行将一些耦合严重的业务模块分出。如果在拆分过程中,拆分模块跟其他模块耦合太严重,那就先放弃这部分模块的独立,毕竟产品是不会单独拿出时间给你做组件化的。
另外拆分的粒度需要大一点,需要在功能模块的基础上,将业务独立性考虑进去,如果没有就不拆,等以后有了相对独立的模块之后再拆。
2.3 组件化第三步-对外服务接口最小化
组件化不是一蹴而就的,我们在完成第二步的时候并不要强行要求去掉组件之间代码的硬依赖,只需要保证单独拆分出来的工程可以独立运行和测试,并且能够通过引用保证其他业务组件和主工程的依赖使用即可。
当第二步完成之后,我们可以在此基础上总结其他组件和主工程的需求调用,根据需求总结和抽象出当前业务组件对外服务的最小化接口以及页面跳转调用。经过多次总结,我们可以发现组件之间的通信需求无外乎三个方面:URL导航+服务接口调用+消息变量定义。如下图所示:
在这个阶段,我们大多数应用会选择JLRoute(蘑菇街的MGJRoute方案也类似)去做URL导航的需求,会通过OpenServiceImpl + Protocol的方案(将所有对外服务提供的接口都在OpenServiceImpl中实现)去做组件间的服务调用,消息变量的声明可以放到对外服务接口的Protocol定义中。
到了这个阶段,我们的业务组件也已经相对独立,JLRoute能够去掉页面引用的头头文件依赖。OpenServiceImpl+Protocol也将我们最小化的对外服务接口约束到Protocol接口文件中。 如果对于项目组件化要求不高的话,到这一步就可以了。
三、彻底组件化-LDBusMediator炼就
3.1 组件化方案不彻底之处和JLRoute的缺陷
通过第二部分的讲述,我们的组件化工作差不多完成了80%,但是我们依然发现,组件化并不够彻底。
先来看服务调用方面,我们需要对外提供OpenServiceImpl的头文件,外部模块仍然保持着对业务组件的强依赖,OpenServiceImpl的不兼容变化必然导致所有调用部分的更改,我们期望的黑盒服务便无法实现。如果所有类别的服务接口都在OpenServiceImpl中实现,OpenServiceImpl中的代码会越来越多,难以维护和管理。 另外Protocol文件和OpenServiceImpl的头文件都需要对外披露,如果放到组件实现中,两个组件相互之间有调用,就会导致Podspec的相互循环依赖。
再看URL导航方面,在我们的项目中,我们在ViewController的类别中通过load方法注册URL-Block,这样能够解决JLRoute的中心化注册问题,但是JLRoute仍然存在其他一些缺陷。JLRoute去中心化的具体使用方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
+ (void)load { @autoreleasepool { [JLRoutes addRoute:@"/xxxx" handler:^BOOL(NSDictionary *parameters) { UIViewController *baseViewController = parameters[kLDRouteViewControllerKey]; if (!baseViewController) { baseViewController = [UIViewController topmostViewController]; } if (!baseViewController) { return YES; } XXXXViewController *viewController = [[XXXXViewController alloc] init]; if ([baseViewController isKindOfClass:[UINavigationController class]]) { [(UINavigationController*)baseViewController pushViewController:viewController animated:YES]; }else if (baseViewController.navigationController) { [baseViewController.navigationController pushViewController:viewController animated:YES]; } else { UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:viewController]; [baseViewController presentViewController:navController animated:YES completion:NULL]; } return YES; }]; } } |
如上所用,JLRoute的缺陷如下:
- url短链分布式注册时,导航代码的重复拷贝;
- 无法通过URL返回一个controller实例;(TabController也就无法从独立业务组件中不引用Controller头文件获取Controller实例完成设置)
- class的load方法完成注册,太多对启动时Main线程有影响;
- 同一个url短链的导航方式单一固定,依赖注册
- 单一业务组件中可导航URL分散,无法统一查看;
- Debug阶段url传递参数错误、not found没有提示;
3.2 LDBusMediator总体方案
针对组件化不彻底的实际问题,结合之前手淘分享的总线架构以及蘑菇街的组件化分享博客,我们完成了一个通用的LDBusMediator中间件帮助我们彻底完成组件化。
LDBusMediator开源Git地址:
我们先来看总体的组件化方案:所有的业务组件通过Connector连接到总线中,Connector需要遵循Connector Protocol方可接入。Connector协议规定了URL导航接入和服务接入的协议,Connector通过Class的Load方法将自己的实例注册到中间件的Cache数组中,方便其他组件在调用时中间件可以通过服务发现的方式进行URL导航和服务调用。(具体见如下的图示)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@implementation Connector_A #pragma mark - register connector /** * 每个组件的实现必须自己通过load完成挂载; * load只需要在挂载connector的时候完成当前connecotor的初始化,挂载量、挂载消耗、挂载所耗内存都在可控范围内; */ +(void)load{ @autoreleasepool{ [LDBusMediator registerConnector:[self sharedConnector]]; } } @end |
3.3 LDBusMediator-URL导航方案
URL导航的总线中间件方案很简单,只需要在Connector中实现URL导航接入的接口即可,如图所示:
具体使用如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ">15 16 17 18 19 ,这便是中小型应用考虑组件化的时机了。
为了更好的分工协作,团队会安排团队成员各自维护一个相对独立的业务组件。这个时候我们引入组件化方案,一是为了解除组件之间相互引用的代码硬依赖,二是为了规范组件之间的通信接口; 让各个组件对外都提供一个黑盒服务,而组件工程本身可以独立开发测试,减少沟通和维护成本,提高效率。 进一步发展,当团队涉及到转型或者有了新的立项之后,一个团队会开始维护多个项目App,而多个项目App的需求模块往往存在一定的交叉,而这个时候组件化给我们的帮助会更大,我只需要将之前的多个业务组件模块在新的主App中进行组装即可快速迭代出下一个全新App。 二、如何开始组件化工作2.1 组件化的架构目标在详细说如何具体开始组件化工作之前,我们对于组件化的期望应该是这样的,一个团队维护一到两个独立App,每个独立App除开包含一些产品相关的非独立模块集之外,还需要用一些独立的业务组件进行组装。 而不管是产品的非独立模块集、还是独立业务组件都需要底层公共库和基础库的支持。如下图所示: 组件化目标图.png
2.2 组件化第一步-剥离公共库和产品基础库在具体的项目开发过程中,我们使用cocoapod的组件依赖管理利器已经开始从Github上引入了一些第三方开源的基础库,比如说AFNetworking、SDWebImage、SVProgressHUD、ZipArchive等。除开这些第三方开源基础库之外,我们还需要做的事情就是将一些基础组件从主工程剥离出来,形成产品自己的私有基础库仓库,为我们进行业务独立组件的分离做准备。 这部分我将其分为两类:一类是公共基础库,用于跨产品使用;一类是产品基础库,在某个产品中强相关依赖使用。这里以我们自己产品划分为例,概述一下这两类库都包括哪些基础组件: 公共库包括:组件化中间件、网络诊断、第三方SDK管理封装、长连接相关、Patch相关、网络和页面监控相关、用户行为统计库、第三方分享库、JSBridge相关、关于Device+file+crypt+http的基础方法等。 产品基础库包括:通用的WebViewContainer组件(封装了JSBridge)、自定义数字键盘、表情键盘、自定义下拉列表、循环滚动页面、AFNeworking封装库(对上层业务隐藏AF的直接引用)、以及其他自定义的UI基础组件库。 2.2 组件化第二步-独立业务模块单独成库在基础库成体系的基础上,我们就可以开始按照需求定性将一些相对独立的业务模块独立成库,单独在一个工程上进行开发、测试。 往往在这个阶段有一个误区,千万不能为了组件化而强行将一些耦合严重的业务模块分出。如果在拆分过程中,拆分模块跟其他模块耦合太严重,那就先放弃这部分模块的独立,毕竟产品是不会单独拿出时间给你做组件化的。 另外拆分的粒度需要大一点,需要在功能模块的基础上,将业务独立性考虑进去,如果没有就不拆,等以后有了相对独立的模块之后再拆。 2.3 组件化第三步-对外服务接口最小化组件化不是一蹴而就的,我们在完成第二步的时候并不要强行要求去掉组件之间代码的硬依赖,只需要保证单独拆分出来的工程可以独立运行和测试,并且能够通过引用保证其他业务组件和主工程的依赖使用即可。 当第二步完成之后,我们可以在此基础上总结其他组件和主工程的需求调用,根据需求总结和抽象出当前业务组件对外服务的最小化接口以及页面跳转调用。经过多次总结,我们可以发现组件之间的通信需求无外乎三个方面:URL导航+服务接口调用+消息变量定义。如下图所示: 组件通信需求.png
在这个阶段,我们大多数应用会选择JLRoute(蘑菇街的MGJRoute方案也类似)去做URL导航的需求,会通过OpenServiceImpl + Protocol的方案(将所有对外服务提供的接口都在OpenServiceImpl中实现)去做组件间的服务调用,消息变量的声明可以放到对外服务接口的Protocol定义中。 到了这个阶段,我们的业务组件也已经相对独立,JLRoute能够去掉页面引用的头头文件依赖。OpenServiceImpl+Protocol也将我们最小化的对外服务接口约束到Protocol接口文件中。 如果对于项目组件化要求不高的话,到这一步就可以了。 三、彻底组件化-LDBusMediator炼就3.1 组件化方案不彻底之处和JLRoute的缺陷通过第二部分的讲述,我们的组件化工作差不多完成了80%,但是我们依然发现,组件化并不够彻底。 先来看服务调用方面,我们需要对外提供OpenServiceImpl的头文件,外部模块仍然保持着对业务组件的强依赖,OpenServiceImpl的不兼容变化必然导致所有调用部分的更改,我们期望的黑盒服务便无法实现。如果所有类别的服务接口都在OpenServiceImpl中实现,OpenServiceImpl中的代码会越来越多,难以维护和管理。 另外Protocol文件和OpenServiceImpl的头文件都需要对外披露,如果放到组件实现中,两个组件相互之间有调用,就会导致Podspec的相互循环依赖。 再看URL导航方面,在我们的项目中,我们在ViewController的类别中通过load方法注册URL-Block,这样能够解决JLRoute的中心化注册问题,但是JLRoute仍然存在其他一些缺陷。JLRoute去中心化的具体使用方式如下:
如上所用,JLRoute的缺陷如下:
3.2 LDBusMediator总体方案针对组件化不彻底的实际问题,结合之前手淘分享的总线架构以及蘑菇街的组件化分享博客,我们完成了一个通用的LDBusMediator中间件帮助我们彻底完成组件化。 LDBusMediator开源Git地址: 我们先来看总体的组件化方案:所有的业务组件通过Connector连接到总线中,Connector需要遵循Connector Protocol方可接入。Connector协议规定了URL导航接入和服务接入的协议,Connector通过Class的Load方法将自己的实例注册到中间件的Cache数组中,方便其他组件在调用时中间件可以通过服务发现的方式进行URL导航和服务调用。(具体见如下的图示)
3.3 LDBusMediator-URL导航方案URL导航的总线中间件方案很简单,只需要在Connector中实现URL导航接入的接口即可,如图所示: DBusMediator-URL导航.png
具体使用如下: |