前言
首先我觉得”组件”在这里不太合适,因为按我理解组件是指比较小的功能块,这些组件不需要多少组件间通信,没什么依赖,也就不需要做什么其他处理,面向对象就能搞定。而这里提到的是较大粒度的业务功能,我们习惯称为”模块”,指较大粒度的业务模块。
为什么需要进行组件化
【1】产品闭环已经确定,就需要实施组件化来应对A轮之后的业务扩张。
【2】将项目中的各个模块按照基础组件,功能组件,业务组件划分成一个个单独的模块,以使得各个模块间可以单独开发、测试、组合运行。
【3】出现一些相对独立的业务功能模块,而团队的规模也会随着项目迭代逐渐增长。
为了更好的分工协作,团队会安排团队成员各自维护一个相对独立的业务组件。这个时候我们引入组件化方案,一是为了解除组件之间相互引用的代码硬依赖,二是为了规范组件之间的通信接口; 让各个组件对外都提供一个黑盒服务,而组件工程本身可以独立开发测试,减少沟通和维护成本,提高效率。
【4】相同模块重复开发。
进一步发展,当团队涉及到转型或者有了新的立项之后,一个团队会开始维护多个项目App,而多个项目App的需求模块往往存在一定的交叉,而这个时候组件化给我们的帮助会更大,我只需要将之前的多个业务组件模块在新的主App中进行组装即可快速迭代出下一个全新App。
组件化的期望:
一个团队维护一到两个独立App,每个独立App除开包含一些产品相关的非独立模块集之外,还需要用一些独立的业务组件进行组装。 而不管是产品的非独立模块集、还是独立业务组件都需要底层公共库和基础库的支持。如下图所示:
在最理想的情况下,这些子工程直接应该只存在上层到下层的依赖,即业务模块对底层基础模块的依赖,业务工程之间尽可能不出现横向依赖。
模块设计原则
- 越底层的模块,应该越稳定,越抽象,越具有高复用度。
-
不要让稳定的模块依赖不稳定的模块, 减少依赖。
比如 B 模块依赖了 A 模块,如果 B 模块很稳定,但是 A 模块不稳定,那么B模块也会变的不稳定了
-
提升模块的复用度,自完备性有时候要优于代码复用
我们为了这个模块的自完备性,就可以重新实现下这几个方法,而不是依赖Utils模块
- 每个模块只做好一件事情,不要让Common出现
- 按照你架构的层数从上到下依赖,不要出现下层模块依赖上层模块的现象,业务模块之间也尽量不要耦合
如何做组件化设计
做模块化还是要结合实际业务,对目前APP的功能做一个模块划分,在划分模块的时候还需要关注模块之间的层级。
比如说,在我们项目中,模块被分成了3个层级:基础层、中间层、业务层。
基础层模块:
比如像网络框架、工具类、各种系统类的扩展、持久化、Log、社交化分享这样的模块,这一层的模块我们可以称之为组件,具有很强的可重用性。这些代码不会频繁改动,可以作为基础依赖。
中间层模块:
可以有登录模块、网络层、资源模块等,这一层模块有一个特点是它们依赖着基础组件但又没有很强的业务属性,同时业务层对这层模块的依赖是很强的。做到公共模块下沉。
业务层模块:
就是直接和产品需求对应的模块了,比如类似朋友圈、直播、Feeds流这样的业务功能了。
组件化第一步-剥离公共库和产品基础库
在具体的项目开发过程中,我们使用cocoapod的组件依赖管理利器已经开始从Github上引入了一些第三方开源的基础库,
比如说AFNetworking、SDWebImage、SVProgressHUD、ZipArchive等。除开这些第三方开源基础库之外,
我们还需要做的事情就是将一些基础组件从主工程剥离出来,形成产品自己的私有基础库仓库,为我们进行业务独立组件的分离做准备。
这部分我将其分为两类:
一类是公共基础库,用于跨产品使用;
一类是产品基础库,在某个产品中强相关依赖使用。
这里以我们自己产品划分为例,概述一下这两类库都包括哪些基础组件:
公共库包括:组件化中间件、网络诊断、第三方SDK管理封装、长连接相关、Patch相关、网络和页面监控相关、用户行为统计库、
第三方分享库、JSBridge相关、关于Device+file+crypt+http的基础方法等。
产品基础库包括:通用的WebViewContainer组件(封装了JSBridge)、自定义数字键盘、表情键盘、自定义下拉列表、
循环滚动页面、AFNeworking封装库(对上层业务隐藏AF的直接引用)、以及其他自定义的UI基础组件库。
组件化第二步-独立业务模块单独成库
在基础库成体系的基础上(基础依赖),下面需要对业务模块之间(横向的依赖)进行拆解。这部分是比较难也是容易碰到问题的。
我们可以按照需求定性将一些相对独立的业务模块独立成库,单独在一个工程上进行开发、测试。
往往在这个阶段有一个误区,千万不能为了组件化而强行将一些耦合严重的业务模块分出。如果在拆分过程中,
拆分模块跟其他模块耦合太严重,那就先放弃这部分模块的独立,毕竟产品是不会单独拿出时间给你做组件化的。
另外拆分的粒度需要大一点,需要在功能模块的基础上,将业务独立性考虑进去,如果没有就不拆,等以后有了相对独立的模块之后再拆。
组件化第三步-对外服务接口最小化
组件化不是一蹴而就的,我们在完成第二步的时候并不要强行要求去掉组件之间代码的硬依赖,
只需要保证单独拆分出来的工程可以独立运行和测试,并且能够通过引用保证其他业务组件和主工程的依赖使用即可。
当第二步完成之后,我们可以在此基础上总结其他组件和主工程的需求调用,
根据需求总结和抽象出当前业务组件对外服务的最小化接口以及页面跳转调用。
这样,最后基主工程就相当于剩下一个空壳需要做的就是通过中间件解耦合各业务模块。
CTMediator 方式的组件化
Casa (文章) 对 iOS 组件化方案的讨论
调用方式
先说本地应用调用,本地组件A在某处调用[[CTMediator sharedInstance] performTarget:targetName
action:actionName params:@{...}]向CTMediator发起跨组件调用,CTMediator根据获得的target和action信息,
通过objective-C的runtime转化生成target实例以及对应的action选择子,然后最终调用到目标业务提供的逻辑,完成需求。
在远程应用调用中,远程应用通过openURL的方式,由iOS系统根据info.plist里的scheme配置找到可以响应URL的应用
(在当前我们讨论的上下文中,这就是你自己的应用),应用通过AppDelegate接收到URL之后,
调用CTMediator的openUrl:方法将接收到的URL信息传入。当然,CTMediator也可以用openUrl:options:
的方式顺便把随之而来的option也接收,这取决于你本地业务执行逻辑时的充要条件是否包含option数据。传入URL之后,
CTMediator通过解析URL,将请求路由到对应的target和action,随后的过程就变成了上面说过的本地应用调用的过程了,
最终完成响应。
当决定要实施组件化方案时,对于组件化方案的架构设计优劣直接影响到架构体系能否长远地支持未来业务的发展,
对App的组件化不只是仅仅的拆代码和跨业务调页面,还要考虑复杂和非常规业务参数参与的调度,非页面的跨组件功能调度,
组件调度安全保障,组件间解耦,新旧业务的调用接口修改等问题。
1. target-action做的事情都是跨业务调度的事情,它不是简单地把方法换个位置。你先理解什么事跨业务调度。
2. category的目的是在调度的时候,调度的人不用去考虑应该具体调哪个target-action,以及参数都有哪些,类型都是什么。
category通过函数参数的方式收集参数并生成调用target-action时的param字典。
category不会利用runtime去做事情,真正利用runtime的是CTMediator,真正做事情的是target-action
3. 同一。跨业务调度的时候,A业务有些功能需要B业务帮忙做点儿事。那么B业务要帮忙的事儿就都写在target-action里,
给A业务调度。
既然用runtime就可以解耦取消依赖,那还要Mediator做什么?组件间调用时直接用runtime接口调不就行了,这样就可以没有任何依赖就完成调用:
但这样做的问题是:
- 调用者写起来很恶心,代码提示都没有,每次调用写一坨。
- runtime方法的参数个数和类型限制,导致只能每个接口都统一传一个 NSDictionary。这个 NSDictionary里的key value是什么不明确,需要找个地方写文档说明和查看。
- 编译器层面不依赖其他组件,实际上还是依赖了,直接在这里调用,没有引入调用的组件时就挂了。
把它移到Mediator后:
- 调用者写起来不恶心,代码提示也有了。
- 参数类型和个数无限制,由 Mediator 去转就行了,组件提供的还是一个 NSDictionary 参数的接口,但在Mediator 里可以提供任意类型和个数的参数,像上面的例子显式要求参数 NSString
bookId 和 NSInteger type。 - Mediator可以做统一处理,调用某个组件方法时如果某个组件不存在,可以做相应操作,让调用者与组件间没有耦合。
到这里,基本上能解决我们的问题:各组件互不依赖,组件间调用只依赖中间件Mediator,Mediator不依赖其他组件。接下来就是优化这套写法,有两个优化点:
1. Mediator 每一个方法里都要写 runtime 方法,格式是确定的,这是可以抽取出来的。
2. 每个组件对外方法都要在 Mediator 写一遍,组件一多 Mediator 类的长度是恐怖的。
优化后就成了 casa 的方案,target-action 对应第一点,target就是class,action就是selector,通过一些规则简化动态调用。Category 对应第二点,每个组件写一个 Mediator 的 Category,让 Mediator 不至于太长。
总结起来就是:
1.各组件可以只专注于自身的业务设计,最后通过无侵入的 target-action 方式为外界提供接口调用,这个 target-action 设计的很精妙。
2.组件间通过中间件通信,中间件通过 runtime 和 组件的 target-action 解耦合。不依赖于任何组件。
3. 组件通过中间件的 category 实现对外的接口调用,这部分由提供服务的组件开发者维护,使得外界的调用者不用参与调用的内部逻辑设计,而且具有多处复用的效果,调用者引入中间件即可,这是一种轻依赖,是权衡后的设计。而且通过 category 感官上分离组件接口代码。
组件化解决的痛点和带来的优势
在 iOS Native app 前期开发的时候,如果参与的开发人员也不多,那么代码大多数都是写在一个工程里面的,
这个时候业务发展也不是太快,所以很多时候也能保证开发效率。
但是一旦项目工程庞大以后,开发人员也会逐渐多起来,业务发展突飞猛进,这个时候单一的工程开发模式就会暴露出弊端了。
- 项目内代码文件耦合比较严重
- 容易出现冲突,大公司同时开发一个项目的人多,每次 pull 一下最新代码就会有很多冲突,有时候合并代码需要半个小时左右,
这会耽误开发效率。
- 业务方的开发效率不够高,开发人员一多,每个人都只想关心自己的组件,但是却要编译整个项目,与其他不相干的代码糅合在一起。
调试起来也不方便,即使开发一个很小的功能,都要去把整个项目都编译一遍,调试效率低。
为了解决这些问题,iOS 项目就出现了组件化的概念。所以 iOS 的组件化是为了解决上述这些问题的,
这里与前端组件化解决的痛点不同。
iOS 组件化以后能带来如下的好处:
- 不只提高了代码的复用度,还可以实现真正的功能复用,比如同样的功能模块如果实现了自完备性,可以在多个app中复用
- 加快编译速度,各组件做成 Framwork 这样可以加快编译速度,对源码也可以隐藏起来!
(不用编译主客那一大坨代码了,各个组件都是静态库)
- 自由选择开发姿势(MVC / MVVM / FRP)
- 方便 QA 有针对性地测试
- 提高业务开发效率
- 业务隔离,跨团队开发代码控制和版本风险控制的实现
缺点,模块化当然也有它的缺点:
- 入门门槛较高,新手入门需要的成本也更高
- 工具的使用成本,团队间和模块间的配合成本升高,开发效率短期会降低。
但是从长期的影响来说,带来的好处远大于坏处的,因此模块化仍然是最佳的架构选择。
小结
最终想要达到的理想目标就是主工程就是一个壳工程,其他所有代码都在组件 Pods 里面,主工程的工作就是初始化,加载这些组件的,没有其他任何代码了。
注意:组件化方案不适合在业务不稳定的情况下过早实施,至少要等产品已经经过MVP阶段时才适合实施组件化。因为业务不稳定意味着链路不稳定,在不稳定的链路上实施组件化会导致将来主业务产生变化时,全局性模块调度和重构会变得相对复杂。
*参考文章:
iOS应用架构谈 组件化方案
iOS组件化实践方案-LDBusMediator炼就
浅析 iOS 应用组件化设计
模块化与解耦
iOS 组件化方案探索
组件化架构漫谈
组件化方案调研
一个iOS模块化开发解决方案
iOS组件化文章集合