组件化架构[转]


转自:https://www.jianshu.com/p/67a6004f6930html

这两天更新了一下文章,而且作了一个PDF版的《组件化架构漫谈》,放在我Github上了。PDF上有文章目录,方便阅读,下面是地址。git

若是你以为不错,请把PDF帮忙转到其余群里,或者你的朋友,让更多的人了解组件化架构,衷心感谢!😁github

 
组件化架构PDF

Github地址 : https://github.com/DeveloperErenLiu/ComponentArchitectureBookweb


前段时间公司项目打算重构,准确来讲应该是按以前的产品逻辑重写一个项目😂。在重构项目以前涉及到架构选型的问题,我和组里小伙伴一块儿研究了一下组件化架构,打算将项目重构为组件化架构。固然不是直接拿来照搬,仍是要根据公司具体的业务需求设计架构。sql

在学习组件化架构的过程当中,从不少高质量的博客中学到很多东西,例如蘑菇街李忠casatwybang的博客。在学习过程当中也遇到一些问题,在微博和QQ上和一些作iOS的朋友进行了交流,很是感谢这些朋友的帮助。数据库

本篇文章主要针对于以前蘑菇街提出的组件化方案,以及casatwy提出的组件化方案进行分析,后面还会简单提到滴滴、淘宝、微信的组件化架构,最后会简单说一下我公司设计的组件化架构。编程


 
占位图

组件化架构的由来

随着移动互联网的不断发展,不少程序代码量和业务愈来愈多,现有架构已经不适合公司业务的发展速度了,不少都面临着重构的问题。缓存

在公司项目开发中,若是项目比较小,普通的单工程+MVC架构就能够知足大多数需求了。可是像淘宝、蘑菇街、微信这样的大型项目,原有的单工程架构就不足以知足架构需求了。安全

就拿淘宝来讲,淘宝在13年开启的“All in 无线”战略中,就将阿里系大多数业务都加入到手机淘宝中,使客户端出现了业务的爆发。在这种状况下,单工程架构则已经远远不能知足现有业务需求了。因此在这种状况下,淘宝在13年开启了插件化架构的重构,后来在14年迎来了手机淘宝有史以来最大规模的重构,将其完全重构为组件化架构服务器

蘑菇街的组件化架构

缘由

在一个项目愈来愈大,开发人员愈来愈多的状况下,项目会遇到不少问题。

  • 业务模块间划分不清晰,模块之间耦合度很大,很是难维护。
  • 全部模块代码都编写在一个项目中,测试某个模块或功能,须要编译运行整个项目
 
耦合严重的工程

为了解决上面的问题,能够考虑加一个中间层来协调模块间的调用,全部的模块间的调用都会通过中间层中转。(注意看两张图的箭头方向)

 
添加中间层

可是发现增长这个中间层后,耦合仍是存在的。中间层对被调用模块存在耦合,其余模块也须要耦合中间层才能发起调用。这样仍是存在以前的相互耦合的问题,并且本质上比以前更麻烦了。

架构改进

因此应该作的是,只让其余模块对中间层产生耦合关系,中间层不对其余模块发生耦合
对于这个问题,能够采用组件化的架构,将每一个模块做为一个组件。而且创建一个主项目,这个主项目负责集成全部组件。这样带来的好处是不少的:

  • 业务划分更佳清晰,新人接手更佳容易,能够按组件分配开发任务。
  • 项目可维护性更强,提升开发效率。
  • 更好排查问题,某个组件出现问题,直接对组件进行处理。
  • 开发测试过程当中,能够只编译本身那部分代码,不须要编译整个项目代码。
  • 方便集成,项目须要哪一个模块直接经过CocoaPods集成便可。
 
组件化架构

进行组件化开发后,能够把每一个组件当作一个独立的app,每一个组件甚至能够采起不一样的架构,例如分别使用MVVMMVCMVCS等架构。

MGJRouter方案

蘑菇街经过MGJRouter实现中间层,经过MGJRouter进行组件间的消息转发,从名字上来讲更像是“路由器”。实现方式大体是,在提供服务的组件中提早注册block,而后在调用方组件中经过URL调用block,下面是调用方式。

架构设计

 
MGJRouter组件化架构

MGJRouter是一个单例对象,在其内部维护着一个“URL -> block”格式的注册表,经过这个注册表来保存服务方注册的block,以及使调用方能够经过URL映射出block,并经过MGJRouter对服务方发起调用。

MGJRouter是全部模块的调度中心,负责全部模块的调用、切换、特殊处理等操做,能够用来处理一切模块间发生的关系。除了原生页面的解析外,还能够根据URL跳转H5页面。

在服务方组件中都对外提供一个PublicHeader,在PublicHeader中声明当前模块所提供的全部功能,这样其余模块想知道当前模块有什么功能,直接看PublicHeader便可。每个block都对应着一个URL,调用方能够经过URLblock发起调用。

#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来集成,将全部组件当作二方库集成到项目中。详细的集成技术点在下面“标准组件化架构设计”章节中会讲到。

MGJRouter调用

代码模拟对详情页的注册、调用,在调用过程当中传递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和参数,AndroidiOS都使用这一套URL,能够保持统一性。

基础组件

在项目中存在不少公共部分的东西,例如封装的网络请求、缓存、数据处理等功能,以及项目中所用到的资源文件。

蘑菇街将这些部分也当作组件,划分为基础组件,位于业务组件下层。全部业务组件都使用同一个基础组件,也能够保证公共部分的统一性。

Protocol方案

总体架构

 
Protocol方案的中间件

为了解决MGJRouter方案中URL硬编码,以及字典参数类型不明确等问题,蘑菇街在原有组件化方案的基础上推出了Protocol方案。Protocol方案由两部分组成,进行组件间通讯的ModuleManager类以及MGJComponentProtocol协议类。

经过中间件ModuleManager进行消息的调用转发,在ModuleManager内部维护一张映射表,映射表由以前的"URL -> block"变成"Protocol -> Class"

在中间件中建立MGJComponentProtocol文件,服务方组件将能够用来调用的方法都定义在Protocol中,将全部服务方的Protocol都分别定义到MGJComponentProtocol文件中,若是协议比较多也能够分开几个文件定义。这样全部调用方依然是只依赖中间件,不须要依赖除中间件以外的其余组件。

Protocol方案中每一个组件须要一个PublicHeader,此类负责实现当前组件对应的协议方法,也就是对外提供服务的实现。在程序开始运行时将自身的Class注册到ModuleManager,并将Protocol反射为字符串当作keyProtocol方案须要提早注册服务

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当作值存储。经过ProtocolClass的时候,就是经过ProtocolModuleManager中将Class映射出来。

[ModuleManager registerClass:MGJUserImpl forProtocol:@protocol(MGJUserProtocol)];

调用时经过ProtocolModuleManager中映射出注册的Class,将获取到的Class实例化,并调用Class实现的协议方法完成服务调用。

Class cls = [[ModuleManager sharedInstance] classForProtocol:@protocol(MGJUserProtocol)];
id userComponent = [[cls alloc] init];
NSString *userName = [userComponent getUserName];

项目调用流程

蘑菇街是OpenURLProtocol混用的方式,两种实现的调用方式不一样,但大致调用逻辑和实现思路相似。在OpenURL不能知足需求或调用不方便时,就能够经过Protocol的方式调用。

  1. 在进入程序后,先使用MGJRouter对服务方组件进行注册。每一个URL对应一个block的实现,block中的代码就是服务方对外提供的服务,调用方能够经过URL调用这个服务。

  2. 调用方经过MGJRouter调用openURL:方法,并将被调用代码对应的URL传入,MGJRouter会根据URL查找对应的block实现,从而调用服务方组件的代码进行通讯。

  3. 调用和注册block时,block有一个字典用来传递参数。这样的优点就是参数类型和数量理论上是不受限制的,可是须要不少硬编码的key名在项目中。

内存管理

蘑菇街组件化方案有两种,ProtocolMGJRouter的方式,但都须要进行register操做。Protocol注册的是ClassMGJRouter注册的是Block,注册表是一个NSMutableDictionary类型的字典,而字典的拥有者又是一个单例对象,这样会形成内存的常驻

下面是对两种实现方式内存消耗的分析:

  • 首先说一下MGJRouter方案可能致使的内存问题,因为block会对代码块内部对象进行持有,若是使用不当很容易形成循环引用的问题。
    若是不考虑循环引用的问题,block方案并不会形成太大的内存占用。被保存在字典中是一个block对象,而block自身的实现只是一个结构体,也就至关于字典中存放的是不少结构体,因此内存的占用并非很大。

  • 对于协议这种实现方式,和block内存常驻方式差很少。只是将存储的block对象换成Class对象,若是不是已经实例化的对象,内存占用仍是比较小的。

casatwy组件化方案

总体架构

casatwy组件化方案能够处理两种方式的调用,远程调用和本地调用,对于两个不一样的调用方式分别对应两个接口。

  • 远程调用经过AppDelegate代理方法传递到当前应用后,调用远程接口并在内部作一些处理,处理完成后会在远程接口内部调用本地接口,以实现本地调用为远程调用服务

  • 本地调用由performTarget:action:params:方法负责,但调用方通常不直接调用performTarget:方法CTMediator会对外提供明确参数和方法名的方法,在方法内部调用performTarget:方法和参数的转换。

 
casatwy提出的组件化架构

架构设计思路

casatwy是经过CTMediator类实现组件化的,在此类中对外提供明确参数类型的接口,接口内部经过performTarget方法调用服务方组件的TargetAction。因为CTMediator类的调用是经过runtime主动发现服务的,因此服务方对此类是彻底解耦的。

但若是CTMediator类对外提供的方法都放在此类中,将会对CTMediator形成极大的负担和代码量。解决方法就是对每一个服务方组件建立一个CTMediatorCategory,并将对服务方的performTarget调用放在对应的Category中,这些Category都属于CTMediator中间件,从而实现了感官上的接口分离。

 
casatwy组件化实现细节

对于服务方的组件来讲,每一个组件都提供一个或多个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中定义的ModuleACategory,为其余模块提供了一个获取控制器并跳转的功能,下面是代码实现。因为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采起的是加前缀的方式,从casatwyDemo中也能够看出,其组件ModuleATarget命名为Target_A,能够区分各个组件的Target。被调用的Action命名为Action_nativeFetchDetailViewController:,能够区分模块内的方法与对外提供的方法。

casatwy将类和方法的命名,都统一按照其功能作区分当作前缀,这样很好的将组件相关和组件内部代码进行了划分。

标准组件化架构设计

这个章节叫作“标准组件化架构设计”,对于项目架构来讲并无绝对意义的标准之说。这里说到的“标准组件化架构设计”只是由于采起这样的方式的人比较多,且这种方式相比而言较合理。

在上面文章中提到了casatwy方案的CTMediator,蘑菇街方案的MGJRouterModuleManager,下面统称为中间件。

总体架构

组件化架构中,首先有一个主工程,主工程负责集成全部组件。每一个组件都是一个单独的工程,建立不一样的git私有仓库来管理,每一个组件都有对应的开发人员负责开发。开发人员只须要关注与其相关组件的代码,其余业务代码和其无关,这样来新人也好上手。

组件的划分须要注意组件粒度,粒度根据业务可大可小。组件划分后属于业务组件,对于一些多个组件共同的东西,例如网络、数据库之类的,应该划分到单独的组件或基础组件中。对于图片或配置表这样的资源文件,应该再单独划分一个资源组件,这样避免资源的重复性。

服务方组件对外提供服务,由中间件调用或发现服务,服务对当前组件无侵入性,只负责对传递过来的数据进行解析和组件内调用的功能。须要被其余组件调用的组件都是服务方,服务方也能够调用其余组件的服务。

优势

经过这样的组件划分,组件的开发进度不会受其余业务的影响,能够多个组件单独的并行开发。组件间的通讯都交给中间件来进行,须要通讯的类只须要接触中间件,而中间件不须要耦合其余组件,这就实现了组件间的解耦。中间件负责处理全部组件之间的调度,在全部组件之间起到控制核心的做用

组件化框架清晰的划分了不一样模块,从总体架构上来约束开发人员进行组件化开发。组件化架构在各个模块之间自然造成了一道屏障,避免某个开发人员偷懒直接引用头文件,产生组件间的耦合,破坏总体架构。

使用组件化架构进行开发时,由于每一个人都负责本身的模块,代码提交也只提交本身负责模块的仓库,因此代码冲突的问题会变得不多

假设之后某个业务发生大的改变,须要对相关代码进行重构,能够在单个组件进行重构。组件化架构下降了重构的风险,保证了代码的健壮性。

组件集成

 
组件化架构图

每一个组件都是一个单独的工程,在组件开发完成后上传到git仓库。主工程经过Cocoapods集成各个组件,集成和更新组件时只须要pod update便可。这样就是把每一个组件当作第三方来管理,管理起来很是方便。

Cocoapods能够控制每一个组件的版本,例如在主项目中回滚某个组件到特定版本,就能够经过修改podfile文件实现。选择Cocoapods主要由于其自己功能很强大,能够很方便的集成整个项目,也有利于代码的复用。经过这种集成方式,能够很好的避免在传统项目中代码冲突的问题

集成方式

对于组件化架构的集成方式,我在看完bang的博客后专门请教了一下bang。根据在微博上和bang的聊天以及其余博客中的学习,在主项目中集成组件主要分为两种方式——源码和framework,但都是经过CocoaPods来集成。

不管是用CocoaPods管理源码,仍是直接管理framework,集成方式都是同样的,都是直接进行pod updateCocoaPods操做。

这两种组件集成方案,实践中也是各有利弊。直接在主工程中集成代码文件,能够在主工程中进行调试。集成framework的方式,能够加快编译速度,并且对每一个组件的代码有很好的保密性。若是公司对代码安全比较看重,能够考虑framework的形式,但framework不利于主工程中的调试。

例如手机QQ或者支付宝这样的大型程序,通常都会采起framework的形式。并且通常这样的大公司,都会有本身的组件库,这个组件库每每能够表明一个大的功能或业务组件,直接添加项目中就可使用。关于组件化库在后面讲淘宝组件化架构的时候会提到。

不推荐的集成方式

以前有些项目是直接用workspace的方式集成的,或者直接在原有项目中创建子项目,直接作文件引用。但这两点都是不建议作的,由于没有真正意义上实现业务组件的剥离,只是像以前的项目同样从文件目录结构上进行了划分。

图片集成

对于项目中图片的集成,能够把图片当作一个单独的组件,组件中只存在图片文件,没有任何代码。图片可使用Bundleimage assets进行管理,若是是Bundle就针对不一样业务模块创建不一样的Bundle,若是是image assets,就按照不一样的模块分类创建不一样的assets

Bundleimage assets二者相比,我仍是更推荐用assets的方式,由于assets自身提供不少功能(例如设置图片拉伸范围),并且在打包以后图片会被打包在.cer文件中,不会被看到。(如今也能够经过工具对.cer文件进行解析,获取里面的图片)

使用Cocoapods,全部的资源文件都放置在一个podspec中,主工程能够直接引用这个podspec,假设此podspec名为:Assets,而这个Assetspodspec里面配置信息能够写为:

s.resources = "Assets/Assets.xcassets/ ** / *.{png}"

主工程则直接在podfile文件中加入:

pod 'Assets', :path => '../MainProject/Assets'(这种写法是访问本地的,能够换成git)

这样便可在主工程直接访问到Assets中的资源文件(不局限图片,sqlitejshtml亦可,在s.resources设置好配置信息便可)了。


注意点

若是经过framework等二进制形式,将组件集成到主项目中,须要注意预编译指令的使用。由于预编译指令在打包framework的时候,就已经在组件二进制代码中打包好,到主项目中的时候预编译指令其实已经再也不起做用了,而是已经在打包时按照预编译指令编码为固定二进制。

组件化开发总结

对于项目架构来讲,必定要创建于业务之上来设计架构。不一样的项目业务不一样,组件化方案的设计也会不一样,应该设计最适合公司业务的架构。

架构对比

硬编码

在除蘑菇街Protocol方案外,其余两种方案都或多或少的存在硬编码问题,硬编码若是量比较大的话挺麻烦的。

casatwyCTMediator方案中须要硬编码TargetAction字符串,只不过这个缺陷被封闭在中间件里面了,将这些字符串都统必定义为常量,外界使用不须要接触到硬编码。蘑菇街的MGJRouter的方案也是同样的,也有硬编码URL的问题,蘑菇街可能也作了相似的处理。

调用方式

casatwy和蘑菇街提出的两套组件化方案,大致结构是相似的,三套方案都分为调用方中间件服务方,只是在具体实现过程当中有些不一样。例如Protocol方案在中间件中加入了Protocol文件,casatwy的方案在中间件中加入了Category

三种方案内部都有容错处理,因此三种方案的稳定性都是比较好的,并且均可以拿出来单独运行,在服务方不存在的状况下也不会有问题。

服务方

在三套方案中,服务方组件都对外提供一个PublicHeaderTarget在文件中统必定义对外提供的服务,从文件中就知道服务方能够作什么。

但三套实现方案实现方式却不一样,蘑菇街的两套方案都须要注册操做,不管是Block仍是Protocol都须要注册后才能够提供服务。而casatwy的方案则不须要,直接经过runtime调用。casatwy的方案实现了真正的对服务方解耦,而蘑菇街的两套方案则没有,对服务方和调用方都形成了耦合。

小总结

我认为三套方案中,Protocol方案是调用和维护最麻烦的一套方案。修改组件间通讯方式时须要维护Protocol,在调用时须要将Class给调用方,再由调用方进行实例化及调用操做,这在开发中是很是影响开发效率的。

总结

下面是组件化开发中的一个小总结,也是开发过程当中的一些注意点。

  • MGJRouter方案中,是经过调用OpenURL:方法并传入URL来发起调用。鉴于URL协议名等固定格式,能够经过判断协议名的方式,使用配置表控制H5native的切换配置表能够从后台更新,只须要将协议名更改一下便可。

mgj://detail?id=123456
http://www.mogujie.com/detail?id=123456

假设如今线上的native组件出现严重bug在后台将配置文件中原有的本地URL换成H5URL,并更新客户端配置文件

在调用MGJRouter时传入这个H5URL便可完成切换,MGJRouter判断若是传进来的是一个H5URL就直接跳转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) -> 基础层,其中核心层又分为highlow两部分。可是这种架构会形成核心层太重,基础层太轻的问题。在上面三层架构中会发现,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客户端初期是单工程的普通项目,但随着业务的飞速发展,现有架构并不能承载愈来愈多的业务需求,致使代码间耦合很严重。后期开发团队对其不断进行重构,淘宝iOSAndroid两个平台,除了某个平台特有的一些特性或某些方案不便实施以外,大致架构都是差很少的。

发展历程:

  1. 刚开始是普通的单工程项目,以传统的MVC架构进行开发。随着业务不断的增长,致使项目很是臃肿、耦合严重。

  2. 2013年淘宝开启"all in 无线"计划,计划将淘宝变为一个大的平台,将阿里系大多数业务都集成到这个平台上,形成了业务的大爆发
    淘宝开始实行插件化架构,将每一个业务模块划分为一个组件,将组件以framework二方库的形式集成到主工程。但这种方式并无作到真正的拆分,仍是在一个工程中使用git进行merge,这样还会形成合并冲突、很差回退等问题。

  3. 迎来淘宝移动端有史以来最大的重构,将其重构为组件化架构。将每一个模块当作一个组件,每一个组件都是一个单独的项目,而且将组件打包成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

 
Bundle App(图片来自淘宝技术分享)

淘宝提出Bundle App的概念,能够经过已有组件,进行简单配置后就能够组成一个新的app出来。解决了多个应用业务复用的问题,防止重复开发同一业务或功能。

BundleApp容器即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

相关文章
相关标签/搜索