本文为『移动前线』群在3月10日的分享总结整理而成,转载请注明来自『移动开发前线』公众号。java
嘉宾介绍git
蘑菇街李忠(花名银时,网名 limboy),客户端开发经验,目前主要负责移动端基础架构设计及核心技术难点攻克(以 iOS 为主),为集团全部 App 提供移动端解决方案。 热衷于尝试新技术,并在团队中推广,致力于以优秀的代码、新的理念拓宽工程师的思路和眼界,以提高团队总体做战能力为己任。github
在组件化以前,蘑菇街 App 的代码都是在一个工程里开发的,在人比较少,业务发展不是很快的时候,这样是比较合适的,能必定程度地保证开发效率。后端
慢慢地代码量多了起来,开发人员也多了起来,业务发展也快了起来,这时单一工程开发模式就会显露出一些弊端:架构
耦合比较严重(由于没有明确的约束,「组件」间引用的现象会比较多)并发
容易出现冲突(尤为是使用 Xib,还有就是 Xcode Project,虽然说有脚本能够改善:https://github.com/truebit/xUnique )app
业务方的开发效率不够高(只关心本身的组件,却要编译整个项目,与其余不相干的代码糅合在一块儿)框架
为了解决这些问题,就采起了「组件化」策略。它能带来这些好处:组件化
加快编译速度(不用编译主客那一大坨代码了)单元测试
自由选择开发姿式(MVC / MVVM / FRP)
方便 QA 有针对性地测试
提升业务开发效率
先来看下,组件化以后的一个大概架构:
「组件化」顾名思义就是把一个大的 App 拆成一个个小的组件,相互之间不直接引用。那如何作呢?
实现方式
以 iOS 为例,因为以前就是采用的 URL 跳转模式,理论上页面之间的跳转只需 open 一个 URL 便可。因此对于一个组件来讲,只要定义「支持哪些 URL」便可,好比详情页,大概能够这么作:
[MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) {
NSNumber *id = routerParameters[@"id"];
// create view controller with id // push view controller}];
首页只需调用 [MGJRouter openURL:@"mgj://detail?id=404"] 就能够打开相应的详情页。
那问题又来了,我怎么知道有哪些可用的 URL?为此,咱们作了一个后台专门来管理。
而后能够把这些短链生成不一样平台所需的文件,iOS 平台生成 .{h,m} 文件,Android 平台生成 .java 文件,并注入到项目中。这样开发人员只需在项目中打开该文件就知道全部的可用 URL 了。
目前还有一块没有作,就是参数这块,虽然描述了短链,但真想要生成完整的 URL,还须要知道如何传参数,这个正在开发中。
还有一种状况会稍微麻烦点,就是「组件A」要调用「组件B」的某个方法,好比在商品详情页要展现购物车的商品数量,就涉及到向购物车组件拿数据。
相似这种同步调用,iOS 以前采用了比较简单的方案,仍是依托于 MGJRouter,不过添加了新的方法 - (id)objectForURL:,注册时也使用新的方法进行注册
[MGJRouter registerURLPattern:@"mgj://cart/ordercount" toObjectHandler:^id(NSDictionary *routerParamters){
// do some calculation return @42; }]
使用时 NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"] 这样就拿到了购物车里的商品数。
稍微复杂但更具通用性的方法是使用「协议」 <-> 「类」绑定的方式,仍是以购物车为例,购物车组件能够提供这么个 Protocol
@protocol MGJCart <NSObject>
+ (NSInteger)orderCount;
@end
能够看到经过协议能够直接指定返回的数据类型。而后在购物车组件内再新建个类实现这个协议,假设这个类名为MGJCartImpl,接着就能够把它与协议关联起来 [ModuleManager registerClass:MGJCartImpl forProtocol:@protocol(MGJCart)],对于使用方来讲,要拿到这个MGJCartImpl,须要调用 [ModuleManager classForProtocol:@protocol(MGJCart)]。拿到以后再调用 + (NSInteger)orderCount 就能够了。
那么,这个协议放在哪里比较合适呢?若是跟组件放在一块儿,使用时仍是要先引入组件,若是有多个这样的组件就会比较麻烦了。因此咱们把这些公共的协议统一放到了 PublicProtocolDomain.h 下,到时只依赖这一个文件就能够了。
Android 也是采用相似的方式。
理想中的组件能够很方便地集成到主客中,而且有跟 AppDelegate 一致的回调方法。这也是 ModuleManager 作的事情。
先来看看如今的入口方法:
其中 [MGJApp startApp] 主要负责一些 SDK 的初始化。[self trackLaunchTime] 是咱们打的一个点,用来监测从 main 方法开始到入口方法调用结束花了多长时间。其余的都由 ModuleManager 搞定,loadModuleFromPlist:pathForResource: 方法会读取 bundle 里的一个 plist 文件,这个文件的内容大概是这样的:
每一个 Module 都实现了 ModuleProtocol,其中有一个 - (BOOL)applicaiton:didFinishLaunchingWithOptions: 方法,若是实现了的话,就会被调用。
还有一个问题就是,系统的一些事件会有通知,好比 applicationDidBecomeActive 会有对应的 UIApplicationDidBecomeActiveNotification,组件若是要作响应的话,只需监听这个系统通知便可。但也有一些事件是没有通知的,好比 - application:didRegisterUserNotificationSettings:,这时组件若是也要作点事情,怎么办?
一个简单的解决方法是在 AppDelegate 的各个方法里,手动调一遍组件的对应的方法,若是有就执行。
壳工程
既然已经拆出去了,那拆出去的组件总得有个载体,这个载体就是壳工程,壳工程主要包含一些基础组件和业务SDK,这也是主工程包含的一些内容,因此若是在壳工程能够正常运行的话,到了主工程也没什么问题。不过这里存在版本同步问题,以后会说到。
遇到的问题
因为以前的代码都是在一个工程下的,因此要单独拿出来做为一个组件就会遇到很多问题。首先是组件的划分,当时在定义组件粒度时也花了些时间讨论,到底是粒度粗点好,仍是细点好。粗点的话比较有利于拆分,细点的话灵活度比较高。最终仍是选择粗一点的粒度,先拆出来再说。
假如要把详情页迁出来,就会发现它依赖了一些其余部分的代码,那最快的方式就是直接把代码拷过来,改个名使用。比较简单暴力。提及来比较简单,作的时候也是挺有挑战的,由于正常的业务并不会由于「组件化」而中止,因此开发同窗们须要同时兼顾正常的业务和组件的拆分。
咱们的组件包括第三方库都是经过 Cocoapods 来管理的,其中组件使用了私有库。之因此选择 Cocoapods,一个是由于它比较方便,还有就是用户基数比较大,且社区也比较活跃(活跃到了会时不时地触发 Github 的 rate limit,致使长时间 clone 不下来··· 见此:https://github.com/CocoaPods/CocoaPods/issues/4989#issuecomment-193772935 ),固然也有其余的管理方式,好比 submodule / subtree,在开发人员比较多的状况下,方便、灵活的方案容易占上风,虽然它也有本身的问题。主要有版本同步和更新/编译慢的问题。
假如基础组件作了个 API 接口升级,这个升级会对原有的接口作改动,天然就会升一个中位的版本号,好比原先是 1.6.19,那么如今就变成 1.7.0 了。而咱们在 Podfile 里都是用 ~ 指定的,这样就会出现主工程的 pod 版本升上去了,可是壳工程没有同步到,而后群里就会各类反馈编译不过,并且这个编译不过的长尾有时能拖上两三天。
而后咱们就想了个办法,若是不在壳工程里指定基础库的版本,只在主工程里指定呢,理论上应该可行,只要不出现某个基础库要同时维护多个版本的状况。但实践中发现,壳工程有时会莫名其妙地升不上去,在 podfile 里指定最新的版本又能够升上去,因此此路不通。
还有一个问题是 pod update 时间过长,常常会在 Analyzing Dependency 上卡 10 多分钟,很是影响效率。后来排查下来是跟组件的 Podspec 有关,配置了 subspec,且依赖比较多。
而后就是 pod update 以后的编译,因为是源码编译,因此这块的时间花费也很多,接下去会考虑 framework 的方式。
持续集成
在刚开始,持续集成还不是很完善,业务方升级组件,直接把 podspec 扔到 private repo 里就完事了。这样最简单,但也常常会带来编译通不过的问题。并且这种随意的版本升级也不太能保证质量。因而咱们就搭建了一套持续集成系统,大概如此:
每一个组件升级以前都须要先经过编译,而后再决定是否升级。这套体系看起来不复杂,但在实施过程当中常常会遇到后端的并发问题,致使业务方要么集成失败,要么要等很多时间。并且也没有一个地方能够呈现当前版本的组件版本信息。还有就是业务方对于这种命令行的升级方式接受度也不是很高。
基于此,在通过了几轮讨论以后,有了新版的持续集成平台,升级操做经过网页端来完成。
大体思路是,业务方若是要升级组件,假设如今的版本是 0.1.7,添加了一些 feature 以后,壳工程测试经过,想集成到主工程里看看效果,或者其余组件也想引用这个最新的,就能够在后台手动把版本升到 0.1.8-rc.1,这样的话,原先依赖 ~> 0.1.7 的组件,不会升到 0.1.8,同时想要测试这个组件的话,只要手动把版本调到 0.1.8-rc.1 就能够了。这个过程不会触发 CI 的编译检查。
当测试经过后,就能够把尾部的 -rc.n 去掉,而后点击「集成」,就会走 CI 编译检查,经过的话,会在主工程的 podfile 里写上固定的版本号 0.1.8。也就是说,podfile 里全部的组件版本号都是固定的。
周边设施
无线基础的职能是为集团提供解决方案,只是在蘑菇街 App 里能 work 是远远不够的,因此就须要提供入口,知道有哪些可用组件,而且如何使用,就像这样(目前还未实现)
这就要求组件的负责人须要及时地更新 README / CHANGELOG / API,而且当发生 API 变动时,可以快速通知到使用方。
组件化以后还有一个问题就是资源的重复性,之前在一个工程里的时候,资源均可以很方便地拿到,如今独立出去了,也不知道哪些是公用的,哪些是独有的,索性都放到本身的组件里,这样就会致使包变大。还有一个问题是每一个组件多是不一样的产品经理在跟,而他们极可能只关注于本身关心的页面长什么样,而忽略了总体的样式。公共 UI 组件就是用来解决这些问题的,这些组件甚至能够跨 App 使用。(目前还未实现)
小结
「组件化」是 App 膨胀到必定体积后的解决方案,能必定程度上解决问题,在提升开发效率的过程当中,采坑是不免的,但愿这篇文章可以带来些帮助。
QA环节
Q: 对协议部分也蛮好奇的,Android端有采用这种方法的可能性么?
A: Android咱们有一个commanager, 思路相似,具体实现的话,须要再翻一下...
Q:不一样组件间怎么监督和核查UI和代码资源的公用性,以免浪费?
A: 组件间的监督和核查机制,咱们没有作,目前正在作的是’包大小压缩’,其中会涉及到图片的去重和压缩,是统一作的。
Q: 每一个组件所依赖的基础库是在各自的podfile中,仍是在主工程podfile里面写?
A: 在壳工程的podspec和主工程的podfile里都有,有一个小技巧是能够把壳工程的podspec做为dev pod来开发,这样就能够避免有一份podspec, 又有一份podfile.
Q:你的组件是如何响应URL的?
A: 组件会经过MGJRouter注册URL, 而后设置callback, 在callback里会经过NavigationController push一个VC出来。
Q: 那这个NavigationController是否是要经过参数传进去?
A: 不用,咱们有一个UISkeletonModule模块,经过它能够拿到全局的NavigationController.
Q: 蘑菇街目前的业务下,组件量有多少个?
A: iOS有70多个(包括基础的和业务的),Android更多。
Q: 那么多组件,开发的时候,是否是须要先熟悉70多个呢?
A: 这是个问题,组件分为基础组件和业务组件,业务组件之间不须要互相熟悉,不过基础组件确实须要。因此咱们正在作一个’组件展现平台’,在上面能够看到咱们提供哪些基础组件,都是怎么用的。
Q: 壳工程会将基础库都拷贝一份么,开发时须要将全部组件代码check下来,这样是否是致使代码文件很大?
A: 壳工程会经过pod去拉组件,pod在某个版本以后加入了local cache功能,一样的lib, 就不用再到网上去下了,因此其实仍是蛮快的,代码文件不会大到哪里去吧。
Q:全部的组件都是放在一个工程里么,能把单个组件都弄成一个子工程来管理么,私有pod,而后在主工程里面pod install对应的组件?
A: 组件都是有单独的工程的,而后会放到私有的pod源中进行管理。
Q: 你的组件在注册URL的时候已经实例化过了是么,仍是说你的组件在block张实例化? 若是是后者,那么你注册URL的时机是何时,是在AppDelegatede的didLaunch中调用start方法么?
A: 在注册URL时实例化的,一个模块若是想在didFinishLaunch时作点事情,只要继承ModuleProtocol协议,而后实现该方法就好了,它会在AppDelegate的相应阶段被调用。
Q: 为何还须要一个壳工程,直接放在主工程下面不行么?
A: 壳工程用于“干净”的业务开发,直接放到主工程,而后push到本身的仓库么?那这样的话,主工程的意义就跟壳工程是同样的吧,若是直接就是在主工程下面开发,那各个组件都在这上面开发,就回到了最开始的状态了。
Q: 能够说起一下Model这块怎么处理的么,Model在各组件间会有传递么?
A: Model通常不会在组件间传递,在组件内部却是能够随便传,Model这块咱们是本身写一个MGJEntity, 大体的功能跟JSONModel相似
Q: 组件间数据较多时怎样组件方式传递?
A: 目前尚未遇到这样的状况,若是比较多的话,可能会考虑同步调用,也就是走协议,这样类型就是已知的。组件间通常不传Model, 若是数据格式比较复杂,会考虑走协议,若是比较简单就直接open url了。
Q: 这些组件都是开源的么,作项目的开发者能够看到具体的实现么,实现文件是打包成静态库么?
A: 对内是开放源码的,项目开发者能够看到,以后的打算是在持续集成那边经过编译后自动framework化。
Q: 一个新的需求有什么标准判断是新加一个组件来作仍是在某个关联的组件里作? 有没有这样的状况,某个组件在开发到必定程度会拆分出若干个组件来?
A: 其实没有太严格的标准,对于业务方来讲,若是现有的基础组件不能知足,就会向咱们提需求,而后咱们尽可能知足他们,若是这个需求比较偏,那就会建议他们从新实现一个。
Q: 请问通常组件化的发布周期是?须要业务手动发布吧?
A: 这个因组件而异,对于基础组件来讲,没什么问题就不会去动它,对于业务组件来讲,他们的发布周期是跟着班车走的,升级行为是在持续集成后台的组件管理中进行的
Q: 大家当初作组件化的过程当中,没有考虑过将每一个模块作成一个单独的Framework框架,而后在主框架上组合一块儿么,或者能够说一下这二者的区别么?
A: 有的,目前已经有几个是这么作了,framework化就是调试时会比较麻烦,断点进不去,好处就是能提高编译速度。
移动前线社群开始收集群成员的博客、公众号啦!若是你有博客,欢迎踊跃提交:
https://github.com/pockry/mobile-frontier