随着移动端 App 应用的普及以及各种产品、运营方案的成熟,移动端 App 的构建目的从单一的满足用户需求的阶段,逐步迈向支持产品形态、业务场景,运营方案等多方需求。
在工程的迭代过程中,代码量和业务规模不断增加,团队成员也会增加或变动。这些增量,使 App 相较于形成初期,难点从某种功能是否能够完美实现,转变为团队协同是否高效,代码是否易于调试,单功能的性能是否易于监测,代码质量是否易于审查。
当代码和业务规模达到一定量的时候,经常会出现:
项目编译时间过长;
性能损耗无法追根溯源;
项目内部引用关系复杂,牵一发而动全身;
参与项目的开发人员私自编写快捷工具导致项目臃肿;
封装入参混乱,难以复用;
项目内部数据环境复杂,数据在传递过程中被变更等等问题。
简而言之,工程环境在开发过程中越来越复杂,留下越来越多的历史包袱,导致每次维护老代码或者在老代码的基础上构建新需求时,就像在搭空中楼阁,相信维护过老项目的同行们都深有体会。
在软件开发中,大部分时候,解决问题从来不缺手段,缺的是解决问题的环境。
图:小狸App 中的组件化成果
一个 App 一般由多个业务闭环组成,在一个业务闭环中由多个或者单个视图控制器共同完成一个完整的业务流程。一开始只需要一个简单的入参便能启动这个业务闭环,然后从数据库拿来更多的业务数据在控制器中完成整个业务。
上述模式是一般的业务模式,在这个模式中,业务闭环在数据上通过一个外部参数调起,在视图上通过另一个业务闭环的控制器调起。在控制器的调起过程中,因为一个上一个业务需要知道下一个业务的控制器,以完成进入该控制器的操作,所以产生了代码上的强耦合关系。App 开发中一般采用 Router
来解决业务之间的强耦合。
目前概括起来 iOS 的路由存在两种类型:
一类是通过 URL 字符串绑定一个执行跳转的 block;
另一类是通过运行时和反射机制,通过类名和方法名的字符串,找到对应要执行的方法进行调用。
综合他们的执行方式、维护难度、内存使用情况等多方面的优势及劣势,我们最终选择了 CTMediator
作为技术选型。同时选择该路由的另一个重要原因是因为它可以做到模块间的完全解耦。
[[CTMediator sharedInstance] performTarget:@"AnyTarget" action:@"AnyAction" params:@{} shouldCacheTarget:false];
因为它在运行时通过字符串找到需要调用的类和方法,所以不存在编码时的引用关系。这样一来,各个模块在编码时便可以相互独立,分仓管理,需要集成的时候再统一进行集成。
图:组件化集成方式
在工程业务并不复杂时,往往通过不同的文件夹区分不同的业务模块。实际上,这种业务区分方式并没有明确的边界,随着工程体量的增长容易产生很多灰度区域。对这些灰度区域反复讨论的过程中会影响开发效率。而且这种模棱两可容易产生同种业务写出两份代码的可能性。所以将每个业务也单独使用 podspec 来管理。
图:使用 podspec 管理的登录模块
众所周知,CocoaPods
提供了一个命令pod lib create AnyModule
,可以帮助开发者快速创建 podspec 和 Example 工程。因为前文中介绍的业务组件是完全解耦,所以只需要在 Example 工程模拟参数和跳转动作,并执行对应的路由,就能在 Example 工程中单独启动一个模块。
这样做有很多好处:
在编码时,工程的文件目录中只有当前任务模块的代码,让开发更专注;
在编译时,只需要编译当前任务依赖的代码,编译时间更短;
在调试运行时,只需要一次简单的点击就能进入正在开发的目标业务流程。
图:Example 的编译及调试范围
同时这样做也有其他好处,当某些业务已经被更先进的业务代替时,如果对业务代码没有明确的分界,业务虽然被移除了,代码却依然残留在工程中。这些残留代码有两个走向:
被遗留在工程中最终被大家遗忘,从不运行,成为僵尸代码。
某些部分被其他不相关的业务不完全引用,最后发现这些代码是以前业务的遗留,想要重构剔除,却又无从下手。
通过 CocoaPods 对业务进行分治,可以避免上述两种情况的发生。
此时工程已经拥有了 模块的对外路由 和 能够单独进行调试的模块,按照预想,A模块调用 B模块的时候直接调用 B模块的路由。但是组件化设计的思想是 A模块调用 B模块时需要对 B模块既不引用,也无感知。
这一层作为路由包装层,来联系任一模块和其他模块的路由。
在模块间协同开发的场景下,当 A模块需要调用 B模块时,B模块只需要预先在包装层给 A模块提供出一个接口,供 A模块调用。B模块能同时进行开发工作,所以两个模块的施工流程上没有 block 关系,可以同步进行。同时 A模块接入其他模块时,只需要查一下目标模块提供的接口列表,是否有自己需要的一个,而不用去看目标模块的源代码,这样节省了开发者的时间和精力。
同时,由于CTMediator
解耦是通过字符串 Target-Action 调用的,所以也需要这样一个包装层,以免这种无感知的依赖关系导致无感知的代码误操作。
图:模块间交互原理
巴别塔:当时人类联合起来兴建希望能通往天堂的高塔;为了阻止人类的计划,上帝让人类说不同的语言,使人类相互之间不能沟通,计划因此失败,人类自此各散东西。
如果工程没有对工具类做统一规范,在工程的中后期,会出现很多命名各异、实现相同的工具类。每个人都有一套自己的工具类体系,并且排斥他人封装的工具类(由于命名看不懂或者其他原因),如果没有有效的代码 Review,这种现象很容易发生。这就好比人类与人类之间的语言不通,本来可以公用的部分,因为语言不通,需要翻译一次。本来可以被抽取出来复用的代码由于使用的工具类不同,无法抽取出来,造成需求重复开发,人力浪费成本上升。
为了补全原生 API 的不足或者是为了方便调用,通常都会通过封装一些工具类或者扩展类来提升开发效率,比如说优秀的开源工具库YYKit
。但是 YYKit 并不能完全满足业务需求,所以需要自行封装一些工具类。通过建立一个公用工具库,让项目参与者们达成共识,不再私自封装工具类,让它为每个业务组件提供便捷的基础支撑。由于工具库属于基础组件,处于最底层,所有其他组件都可以依赖它,所以它自身的依赖关系必须要非常简单,而且为了方便沉淀、优化,这个库要单独保存在一个远程仓库中,进行规范的版本管理。
工具库中的工具也分为两大类:
普通工具
公用工具库作为基础组件,独立存在且对整个项目做支撑,其中普通工具会尽量以系统类的Category
做封装。
AOP工具
因为交换了运行时方法,即使不被调用,也可能导致这些工具创建前和创建后,相同的代码相同的输入,输出结果却不一样。
所以在所有业务模块中要都引用这个基础公用工具库,来保证运行时环境的一致性,做到使用 Example 进行开发时和集成进主 App 工程时,代码的表现是一样的。
一家公司的同一个 App 或者多个 App ,本身设计风格存在高度一致性的可能,而且设计部门也希望保持设计风格的一致性。所以无论是 UI设计师,还是开发人员都希望优秀的设计成果能够在不同需求中复用。
由于视图组件需要被业务强引用,而且设计素材要被编译到包中,所以把所有视图组件放都引用到工程里,就和组件化减少不必要的编译项这个目的冲突。如果单个视图组件放进一个仓库,又因为部分组件并不需要太多代码,在效率上反而得不偿失。
最终我们采用了podspec-subspec
的方式,将所有组件放在一个仓库进行管理,同时,将每个组件做成子库。这样业务组件需要用到某一视图组件时,不需要引用整个组件仓库,只需要通过子库的方式引用到该组件。同时,组件在 Example 中写出调用 demo,展示出视图效果和调用方式。就像一个商店一样,集展示、获取方式为一体。随着业务的开展,会有越来越多的完善视图组件沉淀到这个库中,产品或交互通过对 Example 的预览便可以轻松找到自己想要的业务呈现效果。
图:基础库对业务组件的支撑
针对文章开头的那几个问题,组件化就是它们的答案:
项目编译时间过长:
组件化进行模块开发,隔离了开发时不需要编译的模块。
性能损耗无法追根溯源:
项目内部引用关系复杂,牵一发而动全身:
参与项目的开发人员私自编写快捷工具导致项目臃肿:
封装入参混乱,难以复用:
项目内部数据环境复杂,数据在传递过程中被变更等等问题。
模块隔离开发限制了数据链路的长度,让开发者思考自己的传值方式是否正确。
组件化是从作坊式的开发到工业化代码生产转变中的一个必要过程,也是能使其他技术手段规模应用的基础环境。不管是性能监测,性能优化,编译速度优化,包体积优化,代码质量优化,敏捷开发,单元测试等等这些技术手段,在组件化分治后的代码中执行的效果和效率也将远远大于在主工程中统一执行的效率。同时由于其优异的模块隔离效果和灵活的接入方式,也是在同一工程里尝试新技术栈并兼容老技术栈的最安全方案。
一个集中式的代码工程也是通过上述几个步骤进行组件化的,而且分步分阶段来做,可以非常安全的实施。具体方法还需要开发者在实践和思考中自己探索。
Web
前端/ Java
/ 安卓 )、DBA
、算法工程师( OCR
/用户画像/推荐 )、K8s
架构师、产品专家( HR
方向 / 财务方向 )、网络工程师。欢迎加入掌门教育大家庭,一起畅谈技术,分享交流。