我的组件化方案
对于项目架构来说,一定要建立于业务之上来设计架构。不同的项目业务不同,组件化方案的设计也会不同,应该设计最适合公司业务的架构。
架构设计
以我之前公司项目为例,项目是一个地图导航应用,业务层之下的核心模块和基础模块占比较大,涉及到地图SDK、算路、语音等模块。且基础模块相对比较独立,对外提供了很多调用接口。由此可以看出,公司项目是一个重逻辑的项目,不像电商等App偏展示。
项目整体的架构设计是:层级架构+组件化架构,对于具体的实现细节会在下面详细讲解。采取这种结构混合的方式进行整体架构,对于组件的管理和层级划分比较有利,符合公司业务需求。
在设计架构时,我们将整个项目都拆分为组件,组件化程度相当高。用到哪个组件就在工程中通过Podfile进行集成,并通过URLRouter统一所有组件间的通信。
组件化架构是项目的整体框架,而对于框架中每个业务模块的实现,可以是任意方式的架构,MVVM、MVC、MVCS等都是可以的,只要通过MGJRouter将组件间的通信方式统一即可。
分层架构
组件化架构在物理结构上来说是不分层次的,只有组件与组件之间的划分关系。但是在组件化架构的基础上,应该根据项目和业务设计自己的层次架构,这套层次架构可以用来区分组件所处的层次及职责,所以我们设计了层级架构+组件化架构的整体架构。
我公司项目最开始设计的是三层架构:业务层 -> 核心层 (high + low) -> 基础层,其中核心层又分为high和low两部分。但是这种架构会造成核心层过重,基础层过轻的问题,这种并不适合组件化架构。
在三层架构中会发现,low层并没有耦合业务逻辑,在同层级中是比较独立的,职责较为单一和基础。我们对low层下沉到基础层中,并和基础层进行合并。所以架构被重新分为三层架构:业务层 -> 核心层 -> 基础层。之前基础层大多是资源文件和配置文件,在项目中存在感并不高。
在分层架构中,需要注意只能上层对下层依赖,下层对上层不能有依赖,下层中不要包含上层业务逻辑。对于项目中存在的公共资源和代码,应该将其下沉到下层中。
职责划分
在三层架构中,业务层负责处理上层业务,将不同业务划分到相应组件中,例如IM组件、导航组件、用户组件等。业务层的组件间关系比较复杂,会涉及到组件间业务的通信,以及业务层组件对下层组件的引用。
核心层位于业务层下方,为业务层提供业务支持,如网络、语音识别等组件应该划分到核心层。核心层应该尽量减少组件间的依赖,将依赖降到最小。核心层有时相互之间也需要支持,例如经纬度组件需要网络组件提供网络请求的支持,这种是不可避免的。
其他比较基础的模块,都放在基础层当做基础组件。例如AFN、地图SDK、加密算法等,这些组件都比较独立且不掺杂任何业务逻辑,职责更加单一,相对于核心层更底层。可以包含第三方库、资源文件、配置文件、基础库等几大类,基础层组件相互之间不应该产生任何依赖。
在设计各个组件时,应该遵循“高内聚,低耦合”的设计规范,组件的调用应该简单且直接,减少调用方的其他处理。对于核心层和基础层的划分,可以以是否涉及业务、是否涉及同级组件间通信、是否经常改动为参照点。如果符合这几点则放在核心层,如果不符合则放在基础层。
集成方式
新建一个项目后,首先将配置文件、URLRouter、App容器等集成到主工程中,做一些基础的项目配置,随后集成需要的组件即可。项目被整体拆分为组件化架构后,应用对所有组件的集成方式都是一样的,通过Podfile将需要的组件集成到项目中。通过组件化的方式,使得开发新项目速度变得非常快。
在集成业务层和核心层组件后,组件间的通信都是由URLRouter进行通信,项目中不允许直接依赖组件源码。而基础层组件则在集成后直接依赖,例如资源文件和配置文件,这些都是直接在主工程或组件中使用的。第三方库则是通过核心层的业务封装,封装后由URLRouter进行通信,但核心层也是直接依赖第三方库源码的。
组件的集成方式有两种,源码和framework的形式,我们使用framework的方式集成。因为一般都是项目比较大才用组件化的,但大型项目都会存在编译时间的问题,如果通过framework则会大大减少编译时间,可以节省开发人员的时间。
组件间通信
对于组件间通信,我们采用的MGJRouter方案。因为MGJRouter现在已经很稳定了,而且可以满足蘑菇街这样量级的App需求,证明是很好的,没必要自己写一套再慢慢踩坑。
MGJRouter的好处在于,其调用方式很灵活,通过MGJRouter注册并在block中处理回调,通过URL直接调用或者URL+Params字典的方式进行调用。由于通过URL拼接参数或Params字典传值,所以其参数类型没有数量限定,传递比较灵活。在通过openURL:调用后,可以在completionBlock中处理完成逻辑。
MGJRouter有个问题在于,在编写组件间通信的代码时,会涉及到大量的Hardcood。对于Hardcode的问题,蘑菇街开发了一套后台系统,将所有的Router需要的URL和参数名,都定义到这套系统中。我们维护了一个Plist表,内部按不同组件进行划分,包含URL和传参名以及回调参数。
小思考
MGJRouter可以在openURL:时传入一个NSDictionary参数,在接触RAC之后,我在想是不是可以把NSDictionary参数变为RACSignal参数,直接传一个信号过去。
注册MGJRouter:
objc
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id subscriber) {
[subscriber sendNext:@"刘小壮"];
return [RACDisposable disposableWithBlock:^{
NSLog(@"disposable");
}];
}];
[MGJRouter registerURLPattern:@"CTB://UserCenter/getUserInfo" withSignal:signal];
调用MGJRouter:
objc
RACSignal *signal = [MGJRouter openURL:@"CTB://UserCenter/getUserInfo"];
[signal subscribeNext:^(NSString *userName) {
NSLog(@"userName %@", userName);
}];
这种方式是可行的。使用RACSignal方式优点在于,相对于直接传字典过去更加灵活,并且具备RAC的诸多特性。但缺点也不少,信号控制不好乱用的话也很容易挖坑,是否使用还是看团队情况了。
H5和Native通信
在项目中经常会用到H5页面,如果能通过点击H5页面调起原生页面,这样的话Native和H5的融合会更好。所以我们设计了一套H5和Native交互的方案,这套方案可以使用URLRouter的方式调起原生页面,实现方式也很简单,并且这套方案和H5原本的跳转逻辑并不冲突。
通过iOS自带UIWebView创建一个H5页面后,H5可以通过调用下面的JS函数和Native通信。调用时可以传入新的URL,这个URL可以设置为URLRouter的URL。
objc
window.location.href = 'CTB://UserCenter/UserLogin?userName=lxz&WeChatID=lz2046703959';
通过JS刷新H5页面时,会调用下面的代理方法。如果方法返回YES,则会根据URL协议进行跳转。
objc
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
跳转时系统会判断通信协议,如果是HTTP等标准协议,则会在当前页面进行刷新。如果跳转协议在URL Schame中注册,则会通过系统openURL:的方式调用到AppDelegate的系统代理方法中,在代理方法中调用URLRouter,则可以通过H5页面唤起原生页面。
APP Service
在应用启动过程中,通常会做一些初始化操作。有些初始化操作是运行程序所需要的,例如崩溃统计、建立服务器的长连接等。或有的组件会对初始化操作有依赖关系,例如网络组件依赖requestToken等。
对于应用启动时的初始化操作,应该创建一个AppService来统一管理启动操作,将初始化操作都放在里面,包含创建根控制器等。其中有的初始化操作需要尽快执行,有的并不需要立即执行,可以根据不同操作设定优先级,来管理所有初始化操作。
objc
#import
typedef NS_ENUM(NSUInteger, CTBAppServicePriority) {
CTBAppServicePriorityLow,
CTBAppServicePriorityDefault,
CTBAppServicePriorityHigh,
};
@interface CTBAppService : NSObject
+ (instancetype)appService;
- (void)registerService:(dispatch_block_t)serviceBlock
priority:(CTBAppServicePriority)priority;
@end
参数传递
项目中存在很多的模型定义,那组件化后这些模型应该定义在哪呢?
casatwy对模型类的观点是去Model化,简单来说就是用字典代替Model存储数据。这对于组件化架构来说,是解决组件之间数据传递的一个很好的方法。但是去Model的方式,会存在大量的字段读取代码,使用起来远没有模型类方便。
因为模型类是关乎业务的,理论上必须放在业务层也就是业务组件这一层。但是要把模型对象从一个组件中当做参数传递到另一个组件中,模型类放在调用方和被调方的哪个组件都不太合适,而且有可能不只两个组件使用到这个模型对象。这样的话在其他组件使用模型对象,必然会造成引用和耦合。
如果在用到这个模型对象的所有组件中,都分别维护一份相同的模型类,或者各自维护不同结构的模型类,这样之后业务发生改变模型类就会很麻烦,这是不可取的。
设计方案
如果将所有模型类单独拉出来,定义一个模型组件呢?
这个看起来比较可行,将这个定义模型的组件下沉到基础层,模型组件不包含业务,只声明模型对象的类。如果将原来各个组件的模型类定义都拉出来,单独放在一个组件中,可以将原有各组件的Model层变得很轻量,这样对整个项目架构来说也是有好处的。
在通过Router进行组件间调用时,通过字典进行传值,这种方式比较灵活。在组件内部使用Model层时,还是用模型组件中定义的Model类。Model层建议还是用Model对象的形式比较方便,不建议整体使用去Model化的设计。在接收到其他组件传递过来的字典参数时,可以通过Model类提供的初始化方法,或其他转Model框架将字典转为Model对象。
objc
@interface CTBStoreWelfareListModel : NSObject
/**
* 自定义初始化方法
*/
- (instancetype)initWithDict:(NSDictionary *)dict;
@end
我公司持久化方案用的是CoreData,所有模型的定义都在CoreData组件中,则不需要再单独创建一个模型组件。
动态化构想
我公司项目是一个常规的地图类项目,首页和百度、高德等主流地图导航App一样,有很多添加在地图上的控件。有的版本会添加控件上去,而有的版本会删除控件,与之对应的功能也会被隐藏。
所以,有次和组里小伙伴们开会的时候就在考虑,能不能在服务器下发代码对首页进行布局!这样就可以对首页进行动态布局,例如有活动的时候在指定时间显示某个控件,这样可以避免App Store审核慢的问题。又或者线上某个模块出现问题,可以紧急下架出问题的模块。
对于这个问题,我们设计了一套动态配置方案,这套方案可以对整个App进行配置。
配置表设计
对于动态配置的问题,我们简单设计了一个配置表,初期打算在首页上先进行试水,以后可能会布置到更多的页面上。这样应用程序各模块的入口,都可以通过配置表来控制,并且通过Router控制页面间跳转,灵活性非常大。
在第一次安装程序时使用内置的配置表,之后每次都用服务器来替换本地的配置表,这样就可以实现动态配置应用。下面是一个简单设计的配置数据,JSON中配置的是首页的配置信息,用来模拟服务器下发的数据,真正服务器下发的字段会比这个多很多。
objc
{
"status": 200,
"viewList": [
{
"className": "UIButton",
"frame": {
"originX": 10,
"originY": 10,
"sizeWidth": 50,
"sizeHeight": 30
},
"normalImageURL": "http://image/normal.com",
"highlightedImageURL": "http://image/highlighted.com",
"normalText": "text",
"textColor": "#FFFFFF",
"routerURL": "CTB://search/***"
}
]
}
对于服务器返回的数据,我们会创建一套解析器,这个解析器用来将JSON解析并“转换”为标准的UIKit控件。点击后的事件都通过Router进行跳转,所以首页的灵活性和Router的使用程度成正比。
这套方案类似于React Native的方案,从服务器下发页面展示效果,但没有React Native功能那么全。相对而言是一个轻量级的配置方案,主要用于页面配置。
资源动态配置
除了页面的配置之外,我们发现地图类App一般都存在ipa过大的问题,这样在下载时很消耗流量以及时间。所以我们就在想能不能把资源也做到动态配置,在用户运行程序的时候再加载资源文件包。
我们想通过配置表的方式,将图片资源文件都放到服务器上,图片的URL也随配置表一起从服务器获取。在使用时请求图片并缓存到本地,成为真正的网络APP。在此基础上设计缓存机制,定期清理本地的图片缓存,减少用户磁盘占用。
淘宝组件化架构
本章节源自于宗心在阿里技术沙龙上的一次技术分享。
(https://yq.aliyun.com/articles/129)
架构发展
淘宝iOS客户端初期是单工程的普通项目,但随着业务的飞速发展,现有架构并不能承载越来越多的业务需求,导致代码间耦合很严重。后期开发团队对其不断进行重构,将项目重构为组件化架构,淘宝iOS和Android两个平台,除了某个平台特有的一些特性或某些方案不便实施之外,大体架构都是差不多的。
发展历程
刚开始是普通的单工程项目,以传统的MVC架构进行开发。随着业务不断的增加,导致项目非常臃肿、耦合严重。
2013年淘宝开启"all in 无线"计划,计划将淘宝变为一个大的平台,将阿里系大多数业务都集成到这个平台上,造成了业务的大爆发。 淘宝开始实行插件化架构,将每个业务模块划分为一个子工程,将组件以framework二方库的形式集成到主工程。但这种方式并没有做到真正的拆分,还是在一个工程中使用git进行merge,这样还会造成合并冲突、不好回退等问题。
迎来淘宝移动端有史以来最大的重构,将其重构为组件化架构。将每个模块当做一个组件,每个组件都是一个单独的项目,并且将组件打包成framework。主工程通过podfile集成所有组件的framework,实现业务之间真正的隔离,通过CocoaPods实现组件化架构。
架构优势
淘宝是使用git来做源码管理的,在插件化架构时需要尽可能避免merge操作,否则在大团队中协作成本是很大的。而使用CocoaPods进行组件化开发,则避免了这个问题。
在CocoaPods中可以通过podfile很好的配置各个组件,包括组件的增加和删除,以及控制某个组件的版本。使用CocoaPods的原因,很大程度是为了解决大型项目中,代码管理工具merge代码导致的冲突。并且可以通过配置podfile文件,轻松配置项目。
每个组件工程有两个target,一个负责编译当前组件和运行调试,另一个负责打包framework。先在组件工程做测试,测试完成后再集成到主工程中集成测试。
每个组件都是一个独立app,可以独立开发、测试,使得业务组件更加独立,所有组件可以并行开发。下层为上层提供能满足需求的底层库,保证上层业务层可以正常开发,并将底层库封装成framework集成到主工程中。
使用CocoaPods进行组件集成的好处在于,在集成测试自己组件时,可以直接在本地主工程中,通过podfile使用当前组件源码,可以直接进行集成测试,不需要提交到服务器仓库。
淘宝四层架构
淘宝架构的核心思想是一切皆组件,将工程中所有代码都抽象为组件。
淘宝架构主要分为四层,最上层是组件Bundle(业务组件),依次往下是容器(核心层),中间件Bundle(功能封装),基础库Bundle(底层库)。容器层为整个架构的核心,负责组件间的调度和消息派发。
总线设计
总线设计:URL路由+服务+消息。统一所有组件的通信标准,各个业务间通过总线进行通信。
URL总线
通过URL总线对三端进行了统一,一个URL可以调起iOS、Android、前端三个平台,产品运营和服务器只需要下发一套URL即可调用对应的组件。
URL路由可以发起请求也可以接受返回值,和MGJRouter差不多。URL路由请求可以被解析就直接拿来使用,如果不能被解析就跳转H5页面。这样就完成了一个对不存在组件调用的兼容,使用户手中比较老的版本依然可以显示新的组件。
服务提供一些公共服务,由服务方组件负责实现,通过Protocol进行调用。
消息总线
应用通过消息总线进行事件的中心分发,类似于iOS的通知机制。例如客户端前后台切换,则可以通过消息总线分发到接收消息的组件。因为通过URLRouter只是一对一的进行消息派发和调度,如果多次注册同一个URL,则会被覆盖掉。
Bundle App
在组件化架构的基础上,淘宝提出Bundle App的概念,可以通过已有组件,进行简单配置后就可以组成一个新的app出来。解决了多个应用业务复用的问题,防止重复开发同一业务或功能。
Bundle即App,容器即OS,所有Bundle App被集成到OS上,使每个组件的开发就像app开发一样简单。这样就做到了从巨型app回归普通app的轻盈,使大型项目的开发问题彻底得到了解决。
总 结
好多朋友在看完这篇文章后,都问有没有Demo。其实架构是思想上的东西,重点还是理解架构思想。文章中对思想的概述已经很全面了,用多个项目的例子来描述组件化架构。就算提供了Demo,也没法把Demo套在其他工程上用,因为并不一定适合所在的工程。
后来想了一下,我把组件化架构的集成方式,简单写了个Demo,这样可以解决很多人在架构集成上的问题。我把Demo放在我Github上了,用Coding的服务器来模拟我公司私有服务器,直接拿MGJRouter来当Demo工程中的Router。下面是Demo地址。
https://github.com/DeveloperErenLiu/ComponentArchitecture
推荐阅读
iOS汇编快速入门
如何评价 SwiftUI?
从 SwiftUI 谈声明式 UI 与类型系统
在看就点点吧