蜂鸟商家版 iOS 组件化 / 模块化实践总结

零. 前言

“蜂鸟配送商家版”是一款针对商家打造的专业配送软件,有了这款应用,您可使用蜂鸟商家版呼叫全部平台订单及电话订单配送,餐饮、鲜花、蛋糕、生鲜、商超都可配送。超低运费,清晰合理。海量补贴,充值返现。html

以上这段对「蜂鸟商家版」的描述摘自 蜂鸟配送官网,大概能够理解为蜂鸟商家版是一个给广大商家用来发单呼叫配送员的 App。许多同窗可能只据说过「饿了么」外卖应用,可是对支撑起外卖配送的后勤业务「蜂鸟配送」却知之甚少,实际上天天海量的外卖订单都是由蜂鸟配送系统进行处理和配送最终送到消费者手中的。外卖 O2O 是由外卖平台、商户、配送系统这三方合做共同完成的,缺一不可。O2O 最核心的价值就是人与服务的链接,而这种链接最终都是经过配送才得以实现的。ios

自 2016 年末开始我参与蜂鸟商家版的维护工做,除了平常的开发迭代之外,期间还参与推动了项目 Swift 化、项目组件化 / 模块化、非业务组件开源化等技术改造工做,今天这篇文章就给你们分享一下蜂鸟商家版 iOS 的组件化 / 模块化实践过程和本身的心得体会。git

一. 背景分析

蜂鸟商家版 iOS 端代码使用 Git 进行管理,代码托管在内网的 GitLab 上。项目的依赖管理工具是你们比较熟悉的 CocoaPods,除了 RN 模块为了和 Android 组公用采用 Submodule 进行管理外,其余全部的子模块都采用 Pods 库的方式引入。github

1. 存在的问题

在「蜂鸟商家版 iOS 组件化 / 模块化」工做开展以前,项目主要存在以下这些问题:数据库

  • 项目臃肿不堪

在组件化 / 模块化以前,蜂鸟商家版 App 的全部代码 / 资源文件等都是在同一个主工程里的,只有 RN 仓库或组内公用私有库等极少部分代码游离于主工程以外,因此在开发时,每一次都要编译整个项目的全部代码,十分低效。这个问题在独立开发时还不是十分明显,毕竟虽然项目大可是代码只有一我的在提交,因此项目代码量增长也不是那么夸张并且对项目发生的变化比较熟悉。可是当多人协做开发时,这个缺陷就暴露了出来,你们在各自开发不一样的业务时,不只要时刻和他人同步项目变化、读懂他人代码,还要每次编译完整个项目才能对本身所作的一点修改进行调试,效率低下。swift

  • 团队规模变化

我开始参与蜂鸟商家版 iOS 端的维护时,以前只有一个前辈在维护,也就是一我的独立维护一个 App。而后过了没多久,他离职去了另外一家公司,因此又变成了一我的独立维护这个 App。这时候由于是独立开发,因此也不存在什么太大的问题。但随着团队扩大,后面陆续来了几位同事共同负责这个项目的维护工做,你们都在同一个工程上进行业务开发,常常遇到如代码冲突、开发效率低下、职责划分不清、代码管理混乱等问题。安全

  • 业务发展压力

因为公司处在高速发展的阶段,业务增加很快,最直观的表现就是市场 & 客服部门不断接到大量一线使用者的使用反馈或诉求,最后就变成了产品展现给咱们开发人员的一份接一份的 PRD。紧凑的业务开发需求和各类灵活的功能迫使咱们想尽一切可以使用的办法来提升开发效率,提升提测质量。网络

  • 代码管理混乱

当我开始参与这个项目的维护时,这个项目就已是一个 Swift 和 OC 混编的项目了,而后还有 RN 和 H5 代码,能够说是十分复杂了。虽然这不是我厂惟一一个 Swift 和 OC 的混编项目,但绝对是当时 Swift 化最高的一个项目,约 25% 的代码为 Swift。众所周知,Swift 和 OC 的互相调用远不如 Java 和 Kotlin 的互相调用那么顺滑(反正你如今知道了),而且到处藏着危机,暗坑无数,因此迫切须要找一个方式,将 Swift 和 OC 代码进行整理、转换或者分隔。毕竟,这个文件是 OC 下一个文件就是 Swift 这种频繁的思惟转换在业务开发这种本就十分紧张的场景下,会令人十分疲惫,不利于开发工做的顺利进行。架构

2. 怎样去解决

为了解决以上这些问题,咱们曾经进行过以下一些探索:ide

  1. 移除无用的第三方库和资源文件,减小打包时间:效果不明显;
  2. 整理并推进内部 Gitflow 工做流,提升协做效率:有一些效果,但因为项目过大,平常协做仍然吃力;
  3. 研究 Swift 编译时间优化方法,提升编译效率:发现增长编译时间的都是 Swift 的一些经常使用语法糖,若是不用的话,严重下降开发效率,遂放弃;
  4. 在不拆分主工程的状况下,推进项目整个 Swift 化:因为以前维护项目的前辈离职,致使目前的项目开发人员都对原代码不是十分熟悉,不敢妄加改动,加之业务迭代频繁,开发和测试资源都十分紧张,该工做工做推动十分缓慢。

能够发现上述尝试的结果都不是十分理想,在与 iOS 组内大佬们进行一些沟通,听取大佬们的意见后,决定对原项目进行「组件化 / 模块化拆分」工做,它能带来以下这些好处:

  • 加快编译速度,不用再编译组件 / 模块外没有被依赖到的代码;
  • 便于将每一个模块指定给不一样负责人进行管理;
  • 下降合并难度,减少冲突和出错几率,提升业务开发效率;
  • 将 Swift 和 OC 代码进行分离,便于进一步 Swift 化工做的推动;
  • 可为模块编写单元测试,提升工做效率,同时方便测试人员进行有针对性的测试。

二. 目标设定

  • 功能组件独立:保证全部的底层功能组件从主工程抽出,独立与主工程以外,便于复用、业务模块的调用;
  • 业务模块划分与拆解:将业务按对应用途进行划分和拆解,想办法切断各业务之间的强依赖;
  • 全部组件 / 模块独立编译:全部功能组件和业务模块可以独立于主工程进行编译,有各自的 Demo 工程;
  • CocoaPods 发布:在内网 GitLab 进行发布,而且以后对每一个模块用 GitFlow 工做流进行管理和后续发布工做。

三. 计划制定

说到组件化 / 模块化,那么什么是组件化 / 模块化呢?组件化和模块化的区别又在哪里呢?

组件,就是咱们对功能的封装,一个功能就是一个组件,数据库、网络、文件操做、社会化分享等等这些功能都是组件。咱们之因此要搞出组件的概念,是为了可以让咱们的上层业务模块可以随时依赖和调用这些基础功能。组件基本上能够分为基础功能组件、通用 UI 组件、基础业务组件等这几类。因此为了知足上述要求,组件必须具备较高的独立性、扩展性以及复用性。

模块,就是对一系列有内聚性的业务进行整理,将其与其它业务进行切割、拆分,从主工程或原所在位置抽离为一个相对独立的部分。仅仅针对业务而言,好比说咱们能够把订单业务独立为为一个模块,能够把我的中心独立为一个模块,把用户登陆独立为一个模块等,在 App 中的体现就是一个个独立的 Git 仓库。模块化的一个好处是用到时能够搭积木,好比能够多个工程间复用同一个或几个业务模块,好比腾讯的 QQ 和 TIM,除了 UI 界面外 TIM 显然复用了大量现有的原 QQ 工程的业务模块代码,固然,咱们这里暂时并无这个需求。

通过小组会议讨论,咱们的想法是将共用组件独立出来,而后直接按业务对现有主工程进行拆分同时兼顾 Swift 与 OC 分离,大体划分以下表所示:

1. 组件

组件 库名 主要内容
基础(OC) LPDBOCFoundationGarbage 基础的 OC 组件,各类零散的、混乱的视图、组件、控件、常量、OC 宏定义等,全放在这里,供上层调用。和他的库名同样,其本质就大概就是个垃圾桶。
基础(Swift) LPDBPublicModule 基础的 Swift 组件,包含一些公用的 Swift 扩展,和模块间解耦的协议。
网络(OC) LPDBNetwork 网络组件,对 AFNetworking 的浅层封装,同时包含了和网络相关的业务功能。
... ... ...

2. 模块

模块 库名 主要内容
历史(OC) LPDBHistoryModule 历史订单模块,包含和历史订单相关的资源文件、UI、业务逻辑代码等。
登陆(OC) LPDBLoginModule 用户登陆模块,包含和登陆、注册页面相关的资源文件、UI、业务逻辑代码等。
用户中心(OC) LPDBUserCenterModule 用户中心模块,包含和用户我的中心以及状态相关的资源文件、UI、业务逻辑代码等。
... ... ...

3. 关系

按照上面的思路,理想化的模块 / 组件依赖关系图大概是这个样子的:

由于蜂鸟商家版的团队开发人员以前均没有过任何项目的拆分经验,你们也都是摸着石头过河,走一步看一步。因此虽然以上的拆分思路整体是对的,先拆组件后拆业务,但因为各类各样的缘由,一些问题就在接下来的工做实施过程当中暴露了出来。

四. 工做实施

咱们小组主要仍是以业务开发为主,因此组件化 / 模块化工做都是你们抽空闲时间来完成,并无进行硬性的排期和设置 Deadline。按照以前制定的计划,咱们进行了如下这些工做:

1. 功能组件独立

1.1 LPDBOCFoundationGarbage

LPDBOCFoundationGarbage 是咱们项目最早抽出的部分,这个库将和 LPDBPublicModule 一块儿,做为整个工程的最底层,再往下就是。这个库的定位和它的名字同样,就是一个垃圾桶,啥都往里放。其中大体包含如下一些东西:

  • 自定义的 View 和控件,例如:小红点控件、刷新控件、加载控件、Tips 视图等;
  • 自定义的 Controller,例如:基础控制器 BaseViewController、WebView 基础控制器 BaseWebViewController、自定义的弹框 AlertController等;
  • 和业务相关的对基本类型或系统控件的扩展:对 NSObject、UIButton、UIImageView、UILabel 等添加的扩展代码 category;
  • 甚至版本控制模块 LPDBVersionManager 也放在了这里。

由于咱们在进行拆分任务的同时,还在同时维持着项目的开发工做,因此咱们暂时没有精力作细致的拆分工做,只能先把这些零散的部分先放在一块儿进行管理。

1.2 LPDBPublicModule

LPDBPublicModule 是基础的 Swift 组件,这个库主要包含:

  • 一些公用的 Swift 扩展,例如:对 CGFloat、Date、NSString 等系统类型的 extension;
  • 用于模块间解耦的协议。

由于工程内的 Swift 代码大可能是咱们新写的,因此相对旧的 OC 代码而言,整理地更好一些,因此这个仓库干净不少

1.3 LPDBNetwork

LPDBNetwork 网络组件是咱们项目完成 OC 和 Swift 基础部分后最早抽出的部分,刚开始咱们认为这部分仅仅是单纯的业务网络请求操做和对 AFNetworking 的浅层封装,不包含界面 UI 逻辑等。不过当咱们拆解完成后,发现其中还包含了一堆奇怪的东西:

  • 对 AFNetworking 的封装和网络操做的一些定义,例如:LPDBHttpManager、LPDBRequestObject 和 LPDBModel 等;
  • UI 操做,例如:等待视图 LPDBLoadingView 和 网络请求失败的提示等。

这一部分的话,由于都是比较古老的代码,因此当初的开发人员都已经再也不继续维护了,因此在只能是咱们本身进行拆分的状况下,为了防止大的变动致使发生问题,因此没有对这一块进行更细致的拆解工做。毕竟再烂代码也比不能工做的代码要好。

1.4 LPDBUIKit

Swift 的 UI 库,咱们将工程中的一些 Swift 视图和控件收集到了这个项目中,主要包含如下这些内容:

  • 视图,例如:LPDBEmptyDataView、SlideScrollView 等;
  • 控件,例如:SlideTabKit 等。

由于 Swift 代码总量还不是很大,因此这个库的东西目前也不是不少,之后会逐渐丰富起来。

2. 业务模块拆分

完成了上面的组件库的独立工做后,业务模块的拆解就相对轻松一些了,目前咱们主要完成了三个业务模块的拆分工做。

2.1 LPDBHistoryModule

LPDBHistoryModule 历史订单模块,和历史订单页面相关的信息都在该模块中,主要包含如下内容:

  • UI,例如:历史订单界面、历史订单列表 Cell、加载视图等;
  • 数据模型,例如:历史订单模型;
  • 历史订单列表相关的网络请求。

由于该模块相对来讲比较独立,因此拆分过程也比较顺利,主要依赖了 LPDBPublicModule、LPDBNetwork、LPDBOCFoundationGarbage 组件。

2.2 LPDBLoginModule

LPDBLoginModule 用户登陆模块是一个与用户登陆、注册以及用户登陆信息有关的模块,主要包含了如下信息:

  • UI,例如:用户登陆界面、用户注册界面等;
  • 数据模型,例如:用户信息模型、用户信息地址模型等;
  • 登陆与注册相关的网络请求。

该模块相比较历史订单模块复杂了一些,不过仍然比较顺利,主要依赖了 LPDBPublicModule、LPDBOCFoundationGarbage、LPDBNetwork 组件。

2.3 LPDBUserCenterModule

LPDBUserCenterModule 用户中心模块是一个与用户我的中心以及用户信息修改有关的模块,主要包含了如下信息:

  • UI,例如:用户中心界面、用户电话修改界面、用户密码修改界面等;
  • 数据模型,例如:用户详细信息模型、用户信息地址模型等;
  • 用户中心相关的网络请求,例如:修改电话号码、请求验证码等。

该模块主要依赖了 LPDBOCFoundationGarbage 组件和 LPDBLoginModule 模块。

2.4 其它

剩下的其余一些模块仍然处于计划中的状态,暂未进行拆分。到这一步的话,库间依赖关系大体以下图所示:

能够看到其中存在一些不太合理的依赖关系,如 LPDBUserCenterModule 依赖 LPDBLoginModule 模块,也就是所谓的业务模块横向依赖问题,接下来,咱们就要处理这一问题。

3. 解除耦合

因为以前开发过程当中从未有过任何模块化的考量,因此蜂鸟商家版的代码很是杂糅,项目依赖关系十分复杂,主要能够分为如下三类耦合:

  • 界面耦合:App 执行过程当中,硬编码的界面间的跳转行为;
  • 工程耦合:某些模块在运行时须要依赖主工程的代码才能运行或实现完整的功能;
  • 依赖耦合:两个业务模块之间的有依赖。

3.1 模块间组件共用

在拆分业务模块的过程当中,常常发生两个业务模块同时引用某一块业务代码的问题,这时咱们就须要对这一块代码进行理解,首先区分它到底应不该该划分到业务层来?

  • 若是是的话,应该划归到哪个模块中去更合理一些;
  • 若是不是的话,应该将这一部分代码下沉到哪个组件库中去比较合适,或者独立为一个组件。

在 LPDBUserCenterModule 的抽离过程当中就遇到了这个问题,LPDBUserCenterModule 和 LPDBLoginModule 共同依赖了几个和用户信息有关的数据模型,致使须要发生模块间横向依赖,因此咱们将共用的数据模型抽出,而后下沉到了 LPDBOCFoundationGarbage 中。

3.2 模块间耦合

另外一个常常遇到的问题就是跨模块调用代码的问题了,不只是模块与模块间代码的互相调用、模块间页面的跳转,还有模块反向调用主工程代码等问题,这个问题的解决咱们分了三步:

  • 反射调用

由于工程的复杂性和之前代码的不规范,致使咱们在处理切割业务模块时比较痛苦,因此咱们在刚开始抽出模块时采用了一种快速但不太安全的方式进行解耦,好比在 LPDBUserCenterModule 模块中须要调用主工程的 getMiddlePageVC 方法时,咱们用了以下临时解决方案:

if ([[UIApplication sharedApplication].delegate respondsToSelector:@selector(getMiddlePageVC)]) {
    UIViewController *info = [[UIApplication sharedApplication].delegate performSelector:@selector(getMiddlePageVC)];

    ...
}
复制代码

而后在主工程的 中实现这个接口:

// .h
@interface AppDelegate : UIResponder <UIApplicationDelegate>

...
// LPDBUserCenterModule
- (UIViewController *)getMiddlePageVC;

...

@end

// .m
@implementation AppDelegate

...

- (UIViewController *)getMiddlePageVC {
    ...

    return xxx;
}

...

@end
复制代码

这一方案的优势就是灵活,利用 NSClassFromString、performSelector 等方式,可以快速解决各类耦合问题,瞬间切割出模块。但缺点也显而易见,字符串硬编码,维护成本大,去掉了编译器检查,容易翻车。

  • 协议调用

因此天然而然地,当咱们的某个业务模块的拆分工做基本定型时,咱们就开始将第一步中的反射调用方式替换为协议的方式进行调用,好比当 LPDBLoginModule 模块须要调用主工程的 getCoordinate 方法时,示例以下:

id delegate = [[UIApplication sharedApplication] delegate];

if (![delegate conformsToProtocol:@protocol(AppDelegateProtocol)]) {
    return;
}
CLLocationCoordinate2D coordinate = [delegate coordinate];
复制代码

而后在主工程中实现该方法:

// .h
#import "AppDelegate.h"

@import LPDBLoginModule;

@interface AppDelegate (Protocol) <AppDelegateProtocol>

@end

// .m
@implementation AppDelegate (Protocol)

- (CLLocationCoordinate2D)getCoordinate {
    return self.coordinate;
}

@end
复制代码

可是,样的改变并不能完全解决所编写的模块间互相调用的代码缺少编译器检查的问题,而仅仅是对调用方作了判断加上了容错,并不能在编译期就让开发人员察觉到问题,必定要进行测试才能够,因此这种方式也不是十分理想。

  • Lotusoot 解耦工具

那么为了完全解决问题,咱们开发和引入了组件通讯和工具 Lotusoot,调用方式有下列几种可供参考:

  • 服务调用
let lotus = s(AccountLotus.self) 
let accountModule: AccountLotus = LotusootCoordinator.lotusoot(lotus: lotus) as! AccountLotus
accountModule.login(username: "admin", password: "wow") { (error) in
    print(error ?? "")
}
复制代码
  • 短链注册
let error: NSError? = LotusootRouter.register(route: "newproj://account/login") { (lotusootURL) in
    accountModule.showLoginVC(username: "admin", password: "wow")
}
复制代码
  • 短链调用
let param: Dictionary = ["username" : "admin",
                                 "password" : "wow"]

// 无回调 
LotusootRouter.open(route: "newproj://account/login", params: param)
// 有回调
LotusootRouter.open(route: "newproj://account/login", params: param).completion { (error) in
    print(error ?? "open success")
}
// ⚠️不推荐的用法,用 ?pram0=xxx 这样的形式致使字符串散落在各处,不易管理。
// 但为了保证 Hybrid 项目中 H5 页面的正常跳转,提供了此种调用
LotusootRouter.open(url: "newproj://account/login?username=zhoulingyu").completion { (error) in
    print(error ?? "open success")
}
复制代码

具体能够参见 iOS 灵活的 模块化/组件化 工具与规范 Lotusoot 解说 一文,在此很少作赘述。相似的工具还有 BeeHiveLPDMvvmRouterKit 等,你们能够自行进一步探索。

最终结构就变成了如图所示的样子:

五. 问题整理

1. 不合理的分层结构和库间依赖

因为参与拆分工做的人员比较缺少组件化经验,因此致使某些库的拆分不是十分合理,某些应该沉入底层的公用 Model 和常量等没有在开始时就放到一个合理的位置。业务模块之间也存在一些不合理的横向依赖,没有进行一个合理的业务边界划分。这些缘由致使咱们在进行拆分工做时常常须要回过头来对已经拆出来的模块和组件从新进行整理和处理,重复劳动量很大。

2. 拆分粒度不适中

某些库好比 LPDBOCFoundationGarbage 比较庞大,而像 LPDBUIKit 这样的库中内容却很是少,这一点的处理上存在问题。若是一个拆分完成的库仍然比较臃肿的化,说明仍然存在细化拆分的必余地。

3. 工做进度难以控制

因为没有能提早制定好详细的进度计划表,加上业务工做的挤压,致使咱们花在组件化 / 模块化工做上的时间比较零散。本意是但愿你们可以灵活安排工做,合理处置业务开发与技术改造工做之间的关系,但效果不是很理想,表现就是组件化 / 模块化工做的进行没有连续性,你们的积极性和工做效率也都不高。

六. 经验总结

1. 工做开始前要进行技术调研

查看和学习一些同类成功的案例资料或者向业内大佬们请教可以对计划的制定带来便利,可以使咱们避免不少错误的设计,少走一些弯路,下降返工率。

2. 制定详细总体规划

在准备做战时,我经常发现定好的计划没有用处,但计划的过程仍必不可少。—— 德怀特·艾森豪威尔

制定详细的总体规划可以在设计阶段就将一些不合理的地方暴露出来,从而拿出解决方案使问题提早获得解决,或者把不合理的内容删减替换掉,例如分层不合理、库间依赖这样的问题,就会减小不少。拿出细致的任务拆分计划和工做量预估,也能更合理地将任务安排到开发人员手中,在提高工做效率的同时也能尽可能避免和业务开发产生冲突。

3. 注意对代码质量的控制

好的代码和编码习惯可以大幅提高项目的可维护性,为以后的工做带来便利。咱们以前旧的 OC 代码比较混乱,基本处于没法维护的状态,拆分起来十分痛苦;而新写的 Swift 代码明显质量要高不少(这真的不是咱们自诩...),拆分起来就顺利多了。

4. 重视信息的文档化

每个拆分出的模块及时添加文档,嫌麻烦的话至少要创建一份通用的 README 模板,每个模块或组件的创建者把模块内容、拆分目的、设计思路等基本信息记录一下,有什么坑或者注意点也能够文档化,使之后的长期项目维护成为可能。

七. 开源成果

咱们在组件化 / 模块化工做期间,产出的一些库和工具放在了 GitHub 上进行开源,给你们一些借鉴的同时,也但愿可以收到你们的意见和建议,提升咱们项目自己的质量:

库名 简介 仓库地址
EFPodsAnalyzer 可视化 Pods 库依赖分析工具 github.com/EyreFree/EF…
EFAutoScrollLabel 一个带跑马灯效果的 UILabel github.com/EyreFree/EF…
Bamboots 一个面向协议的 Swift 网络库 github.com/mmoaay/Bamb…
Lotusoot 灵活的 Swift 组件解耦和通讯工具 github.com/Vegetarians…
bigkeeper 一个 iOS & Android 模块化项目效率提高工具 github.com/BigKeeper/b…
SideNavigation 一个支持侧滑且可自定义的侧边栏 github.com/CNKCQ/SideN…
ViewPagers 一个支持手势的 Segmented Control github.com/CNKCQ/ViewP…

八. 后记

本文基本描述了蜂鸟商家版 App 到目前为止的组件化 / 模块化实践状况,但愿本文可以给您的移动项目演进提供一些借鉴。在此过程当中咱们产出的一些文章、开源库和工具,也但愿能给你们带来必定的帮助或者启发。欢迎你们提出各类反馈和建议或,帮助咱们继续改进和提升。

2017 年末,也就是差很少我参与蜂鸟商家版的维护工做满一年的样子,因为业务调整的缘由这个 App 已经移交给别的团队进行维护了,致使项目的 Swift 化和组件化 / 模块化工做并无所有完成,这一点有些遗憾。不过仍是但愿蜂鸟商家版可以愈来愈好,继续为广大商家朋友们服务。

好消息是,接下来我主要参与蜂鸟团队版 App 的架构工做,这一次咱们根据以前暴露出的问题制定了详细的工做计划,有了蜂鸟商家版的踩坑经验后,我相信这一次咱们必定能顺利完成目标。2018,加油,一块儿拼!

本文编写过程当中参考了如下文章,在此对原做者们表示感谢:

  1. 即时配送网之于外卖O2O,配送的更高境界是社群经营
  2. 谈谈个人理解-组件化/模块化
  3. 蘑菇街 App 的组件化之路
  4. 豆瓣App的模块化实践
  5. 手机天猫解耦之路
  6. 京东iOS客户端组件管理实践

九. 后记的后记

「模块化平常」系列短文,把本身模块化过程当中的踩坑历程分享出来,给有(或者尚未)遇到相似问题的同窗一个参考和帮助:

未完待续...2333


若有任何知识产权、版权问题或理论错误,还请指正。
juejin.im/post/5a620c… 转载请注明原做者及以上信息。