转自:https://www.jianshu.com/p/67a6004f6930html
这两天更新了一下文章,而且作了一个PDF
版的《组件化架构漫谈》,放在我Github
上了。PDF
上有文章目录,方便阅读,下面是地址。git
若是你以为不错,请把PDF帮忙转到其余群里,或者你的朋友,让更多的人了解组件化架构,衷心感谢!😁github
Github地址 : https://github.com/DeveloperErenLiu/ComponentArchitectureBookweb
前段时间公司项目打算重构,准确来讲应该是按以前的产品逻辑重写一个项目😂。在重构项目以前涉及到架构选型的问题,我和组里小伙伴一块儿研究了一下组件化架构,打算将项目重构为组件化架构。固然不是直接拿来照搬,仍是要根据公司具体的业务需求设计架构。sql
在学习组件化架构的过程当中,从不少高质量的博客中学到很多东西,例如蘑菇街李忠、casatwy、bang的博客。在学习过程当中也遇到一些问题,在微博和QQ上和一些作
iOS
的朋友进行了交流,很是感谢这些朋友的帮助。数据库本篇文章主要针对于以前蘑菇街提出的组件化方案,以及casatwy提出的组件化方案进行分析,后面还会简单提到滴滴、淘宝、微信的组件化架构,最后会简单说一下我公司设计的组件化架构。编程
随着移动互联网的不断发展,不少程序代码量和业务愈来愈多,现有架构已经不适合公司业务的发展速度了,不少都面临着重构的问题。缓存
在公司项目开发中,若是项目比较小,普通的单工程+MVC架构
就能够知足大多数需求了。可是像淘宝、蘑菇街、微信这样的大型项目,原有的单工程架构就不足以知足架构需求了。安全
就拿淘宝来讲,淘宝在13年开启的“All in 无线”
战略中,就将阿里系大多数业务都加入到手机淘宝中,使客户端出现了业务的爆发。在这种状况下,单工程架构则已经远远不能知足现有业务需求了。因此在这种状况下,淘宝在13年开启了插件化架构的重构,后来在14年迎来了手机淘宝有史以来最大规模的重构,将其完全重构为组件化架构。服务器
在一个项目愈来愈大,开发人员愈来愈多的状况下,项目会遇到不少问题。
为了解决上面的问题,能够考虑加一个中间层来协调模块间的调用,全部的模块间的调用都会通过中间层中转。(注意看两张图的箭头方向)
可是发现增长这个中间层后,耦合仍是存在的。中间层对被调用模块存在耦合,其余模块也须要耦合中间层才能发起调用。这样仍是存在以前的相互耦合的问题,并且本质上比以前更麻烦了。
因此应该作的是,只让其余模块对中间层产生耦合关系,中间层不对其余模块发生耦合。
对于这个问题,能够采用组件化的架构,将每一个模块做为一个组件。而且创建一个主项目,这个主项目负责集成全部组件。这样带来的好处是不少的:
进行组件化开发后,能够把每一个组件当作一个独立的app,每一个组件甚至能够采起不一样的架构,例如分别使用MVVM
、MVC
、MVCS
等架构。
蘑菇街经过MGJRouter
实现中间层,经过MGJRouter
进行组件间的消息转发,从名字上来讲更像是“路由器”。实现方式大体是,在提供服务的组件中提早注册block
,而后在调用方组件中经过URL
调用block
,下面是调用方式。
MGJRouter
是一个单例对象,在其内部维护着一个“URL -> block”
格式的注册表,经过这个注册表来保存服务方注册的block
,以及使调用方能够经过URL
映射出block
,并经过MGJRouter
对服务方发起调用。
MGJRouter
是全部模块的调度中心,负责全部模块的调用、切换、特殊处理等操做,能够用来处理一切模块间发生的关系。除了原生页面的解析外,还能够根据URL跳转H5页面。
在服务方组件中都对外提供一个PublicHeader,在PublicHeader中声明当前模块所提供的全部功能,这样其余模块想知道当前模块有什么功能,直接看PublicHeader便可。每个block
都对应着一个URL
,调用方能够经过URL
对block
发起调用。
#ifndef UserCenterPublicHeader_h #define UserCenterPublicHeader_h /** 跳转用户登陆界面 */ static const NSString * CTBUCUserLogin = @"CTB://UserCenter/UserLogin"; /** 跳转用户注册界面 */ static const NSString * CTBUCUserRegister = @"CTB://UserCenter/UserRegister"; /** 获取用户状态 */ static const NSString * CTBUCUserStatus = @"CTB://UserCenter/UserStatus"; #endif
在组件内部实现block
的注册工做,以及block
对外提供服务的代码实现。在注册的时候须要注意注册时机,应该保证调用时URL
对应的block
已经注册。
蘑菇街项目使用git
做为版本控制工具,将每一个组件都当作一个独立工程,并创建主项目来集成全部组件。集成方式是在主项目中经过CocoaPods
来集成,将全部组件当作二方库集成到项目中。详细的集成技术点在下面“标准组件化架构设计”
章节中会讲到。
代码模拟对详情页的注册、调用,在调用过程当中传递id
参数。参数传递能够有两种方式,相似于Get请求在URL
后面拼接参数,以及经过字典传递参数。下面是注册的示例代码:
[MGJRouter registerURLPattern:@"mgj://detail?id=id" toHandler:^(NSDictionary *routerParameters) { // 下面能够在拿到参数后,为其余组件提供对应的服务 NSString uid = routerParameters[@"id"]; }];
经过openURL:
方法传入的URL
参数,对详情页已经注册的block
方法发起调用。调用方式相似于GET
请求,URL
地址后面拼接参数。
[MGJRouter openURL:@"mgj://detail?id=404"];
也能够经过字典方式传参,MGJRouter
提供了带有字典参数的方法,这样就能够传递非字符串以外的其余类型参数,例如对象类型参数。
[MGJRouter openURL:@"mgj://detail?" withParam:@{@"id" : @"404"}];
有的时候组件间调用过程当中,须要服务方在完成调用后返回相应的参数。蘑菇街提供了另外的方法,专门来完成这个操做。
[MGJRouter registerURLPattern:@"mgj://cart/ordercount" toObjectHandler:^id(NSDictionary *routerParamters){ return @42; }];
经过下面的方式发起调用,并获取服务方返回的返回值,要作的就是传递正确的URL
和参数便可。
NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"];
这时候会发现一个问题,在蘑菇街组件化架构中,存在了不少硬编码的URL和参数。在代码实现过程当中URL
编写出错会致使调用失败,并且参数是一个字典类型,调用方不知道服务方须要哪些参数,这些都是个问题。
对于这些数据的管理,蘑菇街开发了一个web
页面,这个web
页面统一来管理全部的URL
和参数,Android
和iOS
都使用这一套URL
,能够保持统一性。
在项目中存在不少公共部分的东西,例如封装的网络请求、缓存、数据处理等功能,以及项目中所用到的资源文件。
蘑菇街将这些部分也当作组件,划分为基础组件,位于业务组件下层。全部业务组件都使用同一个基础组件,也能够保证公共部分的统一性。
为了解决MGJRouter
方案中URL
硬编码,以及字典参数类型不明确等问题,蘑菇街在原有组件化方案的基础上推出了Protocol
方案。Protocol
方案由两部分组成,进行组件间通讯的ModuleManager
类以及MGJComponentProtocol
协议类。
经过中间件ModuleManager
进行消息的调用转发,在ModuleManager
内部维护一张映射表,映射表由以前的"URL -> block"
变成"Protocol -> Class"
。
在中间件中建立MGJComponentProtocol
文件,服务方组件将能够用来调用的方法都定义在Protocol
中,将全部服务方的Protocol
都分别定义到MGJComponentProtocol
文件中,若是协议比较多也能够分开几个文件定义。这样全部调用方依然是只依赖中间件,不须要依赖除中间件以外的其余组件。
Protocol
方案中每一个组件须要一个PublicHeader,此类负责实现当前组件对应的协议方法,也就是对外提供服务的实现。在程序开始运行时将自身的Class
注册到ModuleManager
中,并将Protocol
反射为字符串当作key
,Protocol方案须要提早注册服务。
Protocol
方案依然须要提早注册,因为Protocol
方案是返回一个Class
,并将Class
反射为对象再调用方法,这种方式不会直接调用类的内部逻辑。能够将Protocol
方案的Class
注册,都放在对应类的PublicHeader
中,或者专门创建一个RegisterProtocol
类。
建立MGJUserImpl
类当作User
模块的服务类,并在MGJComponentProtocol.h
中定义MGJUserProtocol
协议,由MGJUserImpl
类实现协议中定义的方法,完成对外提供服务的过程。下面是协议定义:
@protocol MGJUserProtocol <NSObject> - (NSString *)getUserName; @end
Class
遵照协议并实现定义的方法,外界经过Protocol
获取的Class
实例化为对象,调用服务方实现的协议方法。
ModuleManager
的协议注册方法,注册时将Protocol
反射为字符串当作存储的key
,将实现协议的Class
当作值存储。经过Protocol
取Class
的时候,就是经过Protocol
从ModuleManager
中将Class
映射出来。
[ModuleManager registerClass:MGJUserImpl forProtocol:@protocol(MGJUserProtocol)];
调用时经过Protocol
从ModuleManager
中映射出注册的Class
,将获取到的Class
实例化,并调用Class
实现的协议方法完成服务调用。
Class cls = [[ModuleManager sharedInstance] classForProtocol:@protocol(MGJUserProtocol)]; id userComponent = [[cls alloc] init]; NSString *userName = [userComponent getUserName];
蘑菇街是OpenURL
和Protocol
混用的方式,两种实现的调用方式不一样,但大致调用逻辑和实现思路相似。在OpenURL
不能知足需求或调用不方便时,就能够经过Protocol
的方式调用。
在进入程序后,先使用MGJRouter
对服务方组件进行注册。每一个URL
对应一个block
的实现,block
中的代码就是服务方对外提供的服务,调用方能够经过URL
调用这个服务。
调用方经过MGJRouter
调用openURL:
方法,并将被调用代码对应的URL
传入,MGJRouter
会根据URL
查找对应的block
实现,从而调用服务方组件的代码进行通讯。
调用和注册block
时,block
有一个字典用来传递参数。这样的优点就是参数类型和数量理论上是不受限制的,可是须要不少硬编码的key
名在项目中。
蘑菇街组件化方案有两种,Protocol
和MGJRouter
的方式,但都须要进行register
操做。Protocol
注册的是Class
,MGJRouter
注册的是Block
,注册表是一个NSMutableDictionary
类型的字典,而字典的拥有者又是一个单例对象,这样会形成内存的常驻。
下面是对两种实现方式内存消耗的分析:
首先说一下MGJRouter
方案可能致使的内存问题,因为block
会对代码块内部对象进行持有,若是使用不当很容易形成循环引用的问题。
若是不考虑循环引用的问题,block
方案并不会形成太大的内存占用。被保存在字典中是一个block
对象,而block
自身的实现只是一个结构体,也就至关于字典中存放的是不少结构体,因此内存的占用并非很大。
对于协议这种实现方式,和block
内存常驻方式差很少。只是将存储的block
对象换成Class
对象,若是不是已经实例化的对象,内存占用仍是比较小的。
casatwy组件化方案能够处理两种方式的调用,远程调用和本地调用,对于两个不一样的调用方式分别对应两个接口。
远程调用经过AppDelegate
代理方法传递到当前应用后,调用远程接口并在内部作一些处理,处理完成后会在远程接口内部调用本地接口,以实现本地调用为远程调用服务。
本地调用由performTarget:action:params:
方法负责,但调用方通常不直接调用performTarget:
方法。CTMediator
会对外提供明确参数和方法名的方法,在方法内部调用performTarget:
方法和参数的转换。
casatwy是经过CTMediator
类实现组件化的,在此类中对外提供明确参数类型的接口,接口内部经过performTarget
方法调用服务方组件的Target
、Action
。因为CTMediator
类的调用是经过runtime
主动发现服务的,因此服务方对此类是彻底解耦的。
但若是CTMediator
类对外提供的方法都放在此类中,将会对CTMediator
形成极大的负担和代码量。解决方法就是对每一个服务方组件建立一个CTMediator
的Category
,并将对服务方的performTarget
调用放在对应的Category
中,这些Category
都属于CTMediator
中间件,从而实现了感官上的接口分离。
对于服务方的组件来讲,每一个组件都提供一个或多个Target
类,在Target
类中声明Action
方法。Target
类是当前组件对外提供的一个“服务类”,Target
将当前组件中全部的服务都定义在里面,CTMediator
经过runtime
主动发现服务。
在Target
中的全部Action
方法,都只有一个字典参数,因此能够传递的参数很灵活,这也是casatwy提出的去Model
化的概念。在Action
的方法实现中,对传进来的字典参数进行解析,再调用组件内部的类和方法。
casatwy为咱们提供了一个Demo,经过这个Demo
能够很好的理解casatwy的设计思路,下面按照个人理解讲解一下这个Demo
。
打开Demo
后能够看到文件目录很是清楚,在上图中用蓝框框出来的就是中间件部分,红框框出来的就是业务组件部分。我对每一个文件夹作了一个简单的注释,包含了其在架构中的职责。
在CTMediator
中定义远程调用和本地调用的两个方法,其余业务相关的调用由Category
完成。
// 远程App调用入口 - (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion; // 本地组件调用入口 - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;
在CTMediator
中定义的ModuleA
的Category
,为其余模块提供了一个获取控制器并跳转的功能,下面是代码实现。因为casatwy的方案中使用performTarget
的方式进行调用,因此涉及到不少硬编码字符串的问题,casatwy采起定义常量字符串来解决这个问题,这样管理也更方便。
#import "CTMediator+CTMediatorModuleAActions.h" NSString * const kCTMediatorTargetA = @"A"; NSString * const kCTMediatorActionNativFetchDetailViewController = @"nativeFetchDetailViewController"; @implementation CTMediator (CTMediatorModuleAActions) - (UIViewController *)CTMediator_viewControllerForDetail { UIViewController *viewController = [self performTarget:kCTMediatorTargetA action:kCTMediatorActionNativFetchDetailViewController params:@{@"key":@"value"}]; if ([viewController isKindOfClass:[UIViewController class]]) { // view controller 交付出去以后,能够由外界选择是push仍是present return viewController; } else { // 这里处理异常场景,具体如何处理取决于产品逻辑 return [[UIViewController alloc] init]; } }
下面是ModuleA
组件中提供的服务,被定义在Target_A
类中,这些服务能够被CTMediator
经过runtime
的方式调用,这个过程就叫作发现服务。
在Target_A
中对传递的参数作了处理,以及内部的业务逻辑实现。方法是发生在ModuleA
内部的,这样就能够保证组件内部的业务不受外部影响,对内部业务没有侵入性。
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params { // 对传过来的字典参数进行解析,并调用ModuleA内部的代码 DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init]; viewController.valueLabel.text = params[@"key"]; return viewController; }
在大型项目中代码量比较大,须要避免命名冲突的问题。对于这个问题casatwy采起的是加前缀的方式,从casatwy的Demo
中也能够看出,其组件ModuleA
的Target
命名为Target_A
,能够区分各个组件的Target
。被调用的Action
命名为Action_nativeFetchDetailViewController:
,能够区分模块内的方法与对外提供的方法。
casatwy将类和方法的命名,都统一按照其功能作区分当作前缀,这样很好的将组件相关和组件内部代码进行了划分。
这个章节叫作“标准组件化架构设计”,对于项目架构来讲并无绝对意义的标准之说。这里说到的“标准组件化架构设计”只是由于采起这样的方式的人比较多,且这种方式相比而言较合理。
在上面文章中提到了casatwy方案的CTMediator
,蘑菇街方案的MGJRouter
和ModuleManager
,下面统称为中间件。
组件化架构中,首先有一个主工程,主工程负责集成全部组件。每一个组件都是一个单独的工程,建立不一样的git
私有仓库来管理,每一个组件都有对应的开发人员负责开发。开发人员只须要关注与其相关组件的代码,其余业务代码和其无关,这样来新人也好上手。
组件的划分须要注意组件粒度,粒度根据业务可大可小。组件划分后属于业务组件,对于一些多个组件共同的东西,例如网络、数据库之类的,应该划分到单独的组件或基础组件中。对于图片或配置表这样的资源文件,应该再单独划分一个资源组件,这样避免资源的重复性。
服务方组件对外提供服务,由中间件调用或发现服务,服务对当前组件无侵入性,只负责对传递过来的数据进行解析和组件内调用的功能。须要被其余组件调用的组件都是服务方,服务方也能够调用其余组件的服务。
经过这样的组件划分,组件的开发进度不会受其余业务的影响,能够多个组件单独的并行开发。组件间的通讯都交给中间件来进行,须要通讯的类只须要接触中间件,而中间件不须要耦合其余组件,这就实现了组件间的解耦。中间件负责处理全部组件之间的调度,在全部组件之间起到控制核心的做用。
组件化框架清晰的划分了不一样模块,从总体架构上来约束开发人员进行组件化开发。组件化架构在各个模块之间自然造成了一道屏障,避免某个开发人员偷懒直接引用头文件,产生组件间的耦合,破坏总体架构。
使用组件化架构进行开发时,由于每一个人都负责本身的模块,代码提交也只提交本身负责模块的仓库,因此代码冲突的问题会变得不多。
假设之后某个业务发生大的改变,须要对相关代码进行重构,能够在单个组件进行重构。组件化架构下降了重构的风险,保证了代码的健壮性。
每一个组件都是一个单独的工程,在组件开发完成后上传到git
仓库。主工程经过Cocoapods
集成各个组件,集成和更新组件时只须要pod update
便可。这样就是把每一个组件当作第三方来管理,管理起来很是方便。
Cocoapods
能够控制每一个组件的版本,例如在主项目中回滚某个组件到特定版本,就能够经过修改podfile
文件实现。选择Cocoapods
主要由于其自己功能很强大,能够很方便的集成整个项目,也有利于代码的复用。经过这种集成方式,能够很好的避免在传统项目中代码冲突的问题。
对于组件化架构的集成方式,我在看完bang的博客后专门请教了一下bang。根据在微博上和bang的聊天以及其余博客中的学习,在主项目中集成组件主要分为两种方式——源码和framework
,但都是经过CocoaPods
来集成。
不管是用CocoaPods
管理源码,仍是直接管理framework
,集成方式都是同样的,都是直接进行pod update
等CocoaPods
操做。
这两种组件集成方案,实践中也是各有利弊。直接在主工程中集成代码文件,能够在主工程中进行调试。集成framework
的方式,能够加快编译速度,并且对每一个组件的代码有很好的保密性。若是公司对代码安全比较看重,能够考虑framework
的形式,但framework
不利于主工程中的调试。
例如手机QQ或者支付宝这样的大型程序,通常都会采起framework
的形式。并且通常这样的大公司,都会有本身的组件库,这个组件库每每能够表明一个大的功能或业务组件,直接添加项目中就可使用。关于组件化库在后面讲淘宝组件化架构的时候会提到。
以前有些项目是直接用workspace
的方式集成的,或者直接在原有项目中创建子项目,直接作文件引用。但这两点都是不建议作的,由于没有真正意义上实现业务组件的剥离,只是像以前的项目同样从文件目录结构上进行了划分。
对于项目中图片的集成,能够把图片当作一个单独的组件,组件中只存在图片文件,没有任何代码。图片可使用Bundle
和image assets
进行管理,若是是Bundle
就针对不一样业务模块创建不一样的Bundle
,若是是image assets
,就按照不一样的模块分类创建不一样的assets
。
Bundle
和image assets
二者相比,我仍是更推荐用assets
的方式,由于assets
自身提供不少功能(例如设置图片拉伸范围),并且在打包以后图片会被打包在.cer
文件中,不会被看到。(如今也能够经过工具对.cer
文件进行解析,获取里面的图片)
使用Cocoapods
,全部的资源文件都放置在一个podspec
中,主工程能够直接引用这个podspec
,假设此podspec
名为:Assets
,而这个Assets
的podspec
里面配置信息能够写为:
s.resources = "Assets/Assets.xcassets/ ** / *.{png}"
主工程则直接在podfile
文件中加入:
pod 'Assets', :path => '../MainProject/Assets'(这种写法是访问本地的,能够换成git)
这样便可在主工程直接访问到Assets
中的资源文件(不局限图片,sqlite
、js
、html
亦可,在s.resources
设置好配置信息便可)了。
若是经过framework
等二进制形式,将组件集成到主项目中,须要注意预编译指令的使用。由于预编译指令在打包framework
的时候,就已经在组件二进制代码中打包好,到主项目中的时候预编译指令其实已经再也不起做用了,而是已经在打包时按照预编译指令编码为固定二进制。
对于项目架构来讲,必定要创建于业务之上来设计架构。不一样的项目业务不一样,组件化方案的设计也会不一样,应该设计最适合公司业务的架构。
在除蘑菇街Protocol
方案外,其余两种方案都或多或少的存在硬编码问题,硬编码若是量比较大的话挺麻烦的。
在casatwy的CTMediator
方案中须要硬编码Target
、Action
字符串,只不过这个缺陷被封闭在中间件里面了,将这些字符串都统必定义为常量,外界使用不须要接触到硬编码。蘑菇街的MGJRouter
的方案也是同样的,也有硬编码URL
的问题,蘑菇街可能也作了相似的处理。
casatwy和蘑菇街提出的两套组件化方案,大致结构是相似的,三套方案都分为调用方、中间件、服务方,只是在具体实现过程当中有些不一样。例如Protocol
方案在中间件中加入了Protocol
文件,casatwy的方案在中间件中加入了Category
。
三种方案内部都有容错处理,因此三种方案的稳定性都是比较好的,并且均可以拿出来单独运行,在服务方不存在的状况下也不会有问题。
在三套方案中,服务方组件都对外提供一个PublicHeader
或Target
,在文件中统必定义对外提供的服务,从文件中就知道服务方能够作什么。
但三套实现方案实现方式却不一样,蘑菇街的两套方案都须要注册操做,不管是Block
仍是Protocol
都须要注册后才能够提供服务。而casatwy的方案则不须要,直接经过runtime
调用。casatwy的方案实现了真正的对服务方解耦,而蘑菇街的两套方案则没有,对服务方和调用方都形成了耦合。
我认为三套方案中,Protocol
方案是调用和维护最麻烦的一套方案。修改组件间通讯方式时须要维护Protocol
,在调用时须要将Class
给调用方,再由调用方进行实例化及调用操做,这在开发中是很是影响开发效率的。
下面是组件化开发中的一个小总结,也是开发过程当中的一些注意点。
MGJRouter
方案中,是经过调用OpenURL:
方法并传入URL
来发起调用。鉴于URL
协议名等固定格式,能够经过判断协议名的方式,使用配置表控制H5
和native
的切换,配置表能够从后台更新,只须要将协议名更改一下便可。
mgj://detail?id=123456
http://www.mogujie.com/detail?id=123456
假设如今线上的native
组件出现严重bug
,在后台将配置文件中原有的本地URL
换成H5
的URL
,并更新客户端配置文件。
在调用MGJRouter
时传入这个H5
的URL
便可完成切换,MGJRouter
判断若是传进来的是一个H5
的URL
就直接跳转webView
。并且URL
能够传递参数给MGJRouter
,只须要MGJRouter
内部作参数截取便可。
casatwy方案和蘑菇街Protocol
方案,都提供了传递明确类型参数的方法。在MGJRouter
方案中,传递参数主要是经过相似GET
请求同样在URL
后面拼接参数,和在字典中传递参数两种方式组成。这两种方式会形成传递参数类型不明确,传递参数类型受限(GET
请求不能传递对象)等问题,后来使用Protocol
方案弥补这个问题。
组件化开发能够很好的提高代码复用性,组件能够直接拿到其余项目中使用,这个优势在下面淘宝架构中会着重讲一下。
对于调试工做,应该放在每一个组件中完成。单独的业务组件能够直接提交给测试提测,这样测试起来也比较方便。最后组件开发完成并测试经过后,再将全部组件更新到主项目,提交给测试进行集成测试便可。
使用组件化架构开发,组件间的通讯都是有成本的。因此尽可能将业务封装在组件内部,对外只提供简单的接口。即“高内聚、低耦合”原则。
把握好组件划分粒度的细化程度,太细则项目过于分散,太大则项目组件臃肿。可是项目都是从小到大的一个发展过程,因此不断进行重构是掌握这个组件的细化程度最好的方式。
下面就说说我公司项目的架构,公司项目是一个地图导航应用,业务层之下的基础组件占比较大,涉及到地图SDK、算路、语音等模块。且基础组件相对比较独立,对外提供了不少调用接口。由此能够看出,公司项目是一个重逻辑的项目,不像电商等App偏展现。
项目基础部分占比比较大,总体的架构设计是:层级架构+组件化架构,对于具体的实现细节会在下面详细讲解。采起这种结构混合的方式进行总体架构,对于组件的管理和层级划分比较有利,符合公司业务需求。
在设计层级架构时,咱们将全部层级的组件都“一视同仁”,用到哪一个组件就在当前组件的Podfile
中引入。这样全部组件都会涉及到和Router
的通讯,通讯方式彻底统一。
对于组件间通讯,咱们采用的MGJRouter
方案。由于MGJRouter
如今已经很稳定了,并且能够知足蘑菇街这样量级的App
需求,证实是很好的,不必本身写一套再慢慢踩坑。
MGJRouter
的好处在于,其调用方式很灵活,经过MGJRouter
注册并在block
中处理回调,经过URL
直接调用或者URL+Params
字典的方式进行调用。调用后有返回值,返回值类型无限定。
因为经过URL
拼接参数或Params
字典传值,因此其参数类型没有数量限定,传递比较灵活。在经过openURL:
调用后,能够在completionBlock
中处理完成逻辑。
MGJRouter
的问题在于,在编写组件间通讯的代码时,会涉及到大量的Hardcood
。对于Hardcode
的问题,咱们将全部涉及到Hardcode
的代码都放在组件的PublicHeader.h
中,这样只须要经过PublicHeader
就知道当前组件所提供的能力。
MGJRouter
能够在openURL:
时传入一个NSDictionary
参数,在接触RAC
以后,我在想是否是能够把NSDictionary
参数变为RACSignal
参数,直接传一个信号过去。
注册MGJRouter
:
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { [subscriber sendNext:@"刘小壮"]; return [RACDisposable disposableWithBlock:^{ NSLog(@"disposable"); }]; }]; [MGJRouter registerURLPattern:@"CTB://UserCenter/getUserInfo" withSignal:signal];
调用MGJRouter
:
RACSignal *signal = [MGJRouter openURL:@"CTB://UserCenter/getUserInfo"]; [signal subscribeNext:^(NSString *userName) { NSLog(@"userName %@", userName); }];
经过将RACSignal
当作参数的方式,能够避免大量参数Hardcode
的问题,参数经过RACSignal
的方式传递。这样能够将参数传递、方法调用等操做,都由一个RACSignal
对象来完成,统一调用方式。并且经过openURL:
拿到RACSignal
后,还能够基于RAC
的特性作其余函数响应式的编程。
组件化架构在物理结构上来讲是不分层次的,只是组件与组件之间的依赖关系。可是在组件化架构的基础上,应该根据项目和业务设计本身的层次架构,这套层次架构能够用来区分组件所处的层次及职责,因此咱们设计了层级架构+组件化架构的架构。
我公司项目最开始设计的是三层架构:业务层 -> 核心层 (high
+ low
) -> 基础层,其中核心层又分为high
和low
两部分。可是这种架构会形成核心层太重,基础层太轻的问题。在上面三层架构中会发现,low
层实际上是能够单独拆出来的,low
层是彻底独立于其余层的,且功能比较基础和独立。
因此能够拆分为四层架构:业务层 -> 核心层 -> 基础层 -> 资源层。以前的基础层大多都是资源文件,因此下沉到资源层。将以前核心层的low
层拆分为基础层,拆分后的核心层为业务层提供业务支撑,封装网络、数据库等核心模块,实现真正意义上的核心。
在分层架构中,须要注意只能上层对下层依赖,下层对上层不能有依赖,下层中不要包含上层业务逻辑。对于项目中存在的公共资源和代码,应该将其下沉到下层中。
在四层架构中,业务层用来处理上层业务,例如我的中心模块、搜索模块、语音处理模块等。这些模块的组件间关系比较复杂,会涉及到业务组件之间的关系,以及业务层对下层核心层组件的引用。
在设计核心层时就须要注意了,核心层须要为上层提供业务支持,并且应该遵循“高内聚,低耦合”的设计规范。核心层组件应该调用方便,并且对上层无依赖,不须要上层作太多的处理便可完成任务。
核心层的设计应该尽可能无耦合,可是并不能作到彻底无耦合。例如核心层的分享和网络两个同级组件,分享可能会调用到网络的代码,例如分享成功后向公司服务器发送请求,这时候就不可避免的产生核心层组件间的引用。
一些和业务无关的模块,也就是纯技术的模块,应该放在基础层中当作基础组件。基础组件包含加密、基础网络库(AFNetworking
)、图片库(SDWebImage
)等模块,在基础层中的各个组件间不该该有任何耦合,若是有耦合也只能发生在核心层和业务层。
基础层的组件应该符合“单一职责”的原则,即当前组件只负责和组件相关的事,不会包含其余不相关的代码。例如分享中不该该包含网络请求,若是包含则放在核心层中。
对于核心层和基础层的划分,能够以是否涉及业务、是否涉及同级组件间通讯、是否常常改动为参照点。若是符合这几点则放在核心层,若是不符合则放在基础层。
最底层的资源层用来存放资源文件、图片、Plist
等和代码无关的东西,资源层的设计也应该设计为组件的形式。图片统一放在一个组件中,Plist
或配置文件单独放在一个组件中,等等。这样可使整个项目都以组件的形式进行开发。
casatwy对模型类的观点是去Model化,简单来讲就是用字典代替Model
存储数据。这对于组件化架构来讲,是解决组件之间数据传递的一个很好的方法。可是去Model的方式,会存在大量的字段读取代码,使用起来远没有模型类方便。
由于模型类是关乎业务的,理论上必须放在业务层也就是业务组件这一层。可是要把模型对象从一个组件中当作参数传递到另外一个组件中,模型类放在调用方和服务方的哪一个组件都不太合适,并且有可能不仅两个组件使用到这个模型对象。这样的话在其余组件使用模型对象,必然会形成引用和耦合。
若是在用到这个模型对象的全部组件中,都分别维护一份相同的模型类,或者各自维护不一样结构的模型类,这样以后业务发生改变模型类就会很麻烦。
若是将全部模型类单独拉出来,定义一个模型组件呢?
这个看起来比较可行,将这个定义模型的组件下沉到核心层,模型组件不包含业务,只声明模型对象的类。若是将原来各个组件的模型类定义都拉出来,单独放在一个组件中,能够将原有各模块的Model层变得很轻量,这样对整个项目架构来讲也是有好处的。
上面只是思考,恰巧我公司持久化方案用的是CoreData
,全部模型的定义都在CoreData
组件中,这样就避免了业务层组件之间由于模型类的耦合。
我公司项目是一个常规的地图类项目,首页和百度、高德等主流地图导航App
同样,有不少布置在地图上的控件。有的版本会添加控件上去,而有的版本会删除控件,与之对应的功能也会被隐藏。
因此,有次和组里小伙伴们开会的时候就在考虑,能不能在服务器下发代码对首页进行布局!这样就能够对首页进行动态布局,例若有活动的时候在指定时间显示某个控件,这样能够避免App Store
审核慢的问题。又或者线上某个模块出现问题,须要紧急下架出问题的模块。
对于这个问题,咱们设计了一套动态配置方案,这套方案能够对整个App
进行配置。
对于动态配置的问题,咱们简单设计了一个配置表,初期打算在首页上先进行试水,之后可能会布置到更多的页面上。这样应用程序各模块的入口,均可以经过配置表来控制,而且经过Router
进行模块间跳转,灵活性很是大。
在第一次安装程序时使用内置的配置表,以后每次都用服务器来替换本地的配置表,这样就能够实现动态配置应用。
下面是一个简单设计的假接口,这个接口里是首页的配置信息,用来模拟服务器下发的数据,真正服务器下发的字段会比这个多不少。
{ "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
的使用程度成正比。
除了页面的配置以外,咱们发现地图类App
通常都存在ipa
过大的问题,这样在下载时很消耗流量以及时间。因此咱们就在想能不能把资源也作到动态配置,在用户运行程序的时候再加载资源文件包。
咱们想经过配置表的方式,将图片资源文件都放到服务器上,图片的URL
也随配置表一块儿从服务器获取。在使用时请求图片并缓存到本地,成为真正的网络APP
。并设计缓存机制,按期清理本地的图片缓存,减小用户磁盘占用。
以前看过滴滴iOS
负责人李贤辉的技术分享,分享的是滴滴iOS
客户端的架构发展历程,下面简单总结一下。
滴滴在最开始的时候架构较混乱。而后在2.0时期重构为MVC
架构,使项目划分更加清晰。在3.0时期上线了新的业务线,这时采用的游戏开发中的状态机机制,暂时能够知足现有业务。
然而在后期不断上线顺风车、代驾、巴士等多条业务线的状况下,现有架构变得很是臃肿,代码耦合严重。从而在2015年开始了代号为“The One”
的方案,这套方案就是滴滴的组件化方案。
滴滴的组件化方案,和蘑菇街方案相似,也是经过私有CocoaPods
来管理各个组件。将整个项目拆分为业务部分和技术部分,业务部分包括专车、拼车、巴士等业务模块,每一个业务模块就是一个单独的组件,使用一个pods
管理。技术部分则分为登陆分享、网络、缓存这样的一些基础组件,分别使用不一样的pods
管理。
组件间通讯经过ONERouter
中间件进行通讯,ONERouter
相似于MGJRouter
,担负起协调和调用各个组件的做用。组件间通讯经过OpenURL
方法,来进行对应的调用。ONERouter
内部保存一份Class-URL
的映射表,经过URL
找到Class
并发起调用,Class
的注册放在+load
方法中进行。
滴滴在组件内部的业务模块中,模块内部使用MVVM+MVCS
混合架构,两种架构都是MVC
的衍生版本。其中MVCS
中的Store
负责数据相关逻辑,例如订单状态、地址管理等数据处理。经过MVVM
中的VM
给控制器瘦身,最后Controller
的代码量就不多了。
滴滴文章中说道首页只能有一个地图实例,这在不少地图导航相关应用中都是这样作的。滴滴首页主控制器持有导航栏和地图,每一个业务线首页控制器都添加在主控制器上,而且业务线控制器背景都设置为透明,将透明部分响应事件传递到下面的地图中,只响应属于本身的响应事件。
由主控制器来切换各个业务线首页,切换页面后根据不一样的业务线来更新地图数据。
本章节源自于宗心在阿里技术沙龙上的一次技术分享
淘宝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
能够请求也能够接受返回值,和MGJRouter
差很少。URL
路由请求能够被解析就直接拿来使用,若是不能被解析就跳转H5
页面。这样就完成了一个对不存在组件调用的兼容,使用户手中比较老的版本依然能够显示新的组件。
服务提供一些公共服务,由服务方组件负责实现,经过Protocol
实现。消息负责统一发送消息,相似于通知也须要注册。
淘宝提出Bundle App
的概念,能够经过已有组件,进行简单配置后就能够组成一个新的app
出来。解决了多个应用业务复用的问题,防止重复开发同一业务或功能。
Bundle
即App
,容器即OS
,全部Bundle App
被集成到OS
上,使每一个组件的开发就像app
开发同样简单。这样就作到了从巨型app
回归普通app
的轻盈,使大型项目的开发问题完全获得了解决。
到目前为止组件化架构文章就写完了,文章确实挺长的,看到这里真是辛苦你了😁。下面留个小思考,把下面字符串复制到微信输入框随便发给一个好友,而后点击下面连接大概也能猜到微信的组件化方案。
weixin://dl/profile
各位能够来我博客评论区讨论,能够讨论文中提到的技术细节,也能够讨论本身公司架构所遇到的问题,或本身独到的看法等等。不管是否是架构师或新入行的iOS开发,欢迎各位以一个讨论技术的心态来讨论。在评论区你的问题能够被其余人看到,这样可能会给其余人带来一些启发。
如今H5
技术比较火,好多应用都用H5
来完成一些页面的开发,H5
的跨平台和实时更新等是很是大的优势,但其性能和交互也是缺点。若是之后客户端可以发展到能够动态部署线上代码,不用打包上线应用市场,直接就能够作到原生应用更新,这样就能够解决原生应用最大的痛点。这段时间公司项目比较忙,有时间我打算研究一下这个技术点😄。
Demo
地址:蘑菇街和casatwy
组件化方案,其Github
上都给出了Demo
,这里就贴出其Github
地址了。
蘑菇街-MGJRouter
casatwy-CTMediator
好多朋友在看完这篇文章后,都问有没有Demo
。其实架构是思想上的东西,重点仍是理解架构思想。文章中对思想的概述已经很全面了,用多个项目的例子来描述组件化架构。就算提供了Demo
,也无法把Demo
套在其余工程上用,由于并不必定适合所在的工程。
后来想了一下,我把组件化架构的集成方式,简单写了个Demo
,这样能够解决不少人在架构集成上的问题。我把Demo
放在我Github上了,用Coding的服务器来模拟我公司私有服务器,直接拿MGJRouter
来当Demo
工程中的Router
。下面是Demo
地址,麻烦各位记得点个start