Tenloy's Blog

[转] iOS的组件化方案

Word count: 14.2kReading time: 50 min
2021/04/29 Share

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

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

一、组件化架构的由来

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

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

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

1.1 原因

在一个项目越来越大,开发人员越来越多的情况下,项目会遇到很多问题。

  • 业务模块间划分不清晰,模块之间耦合度很大,非常难维护。
  • 所有模块代码都编写在一个项目中,测试某个模块或功能,需要编译运行整个项目

为了解决上面的问题,可以考虑加一个中间层来协调各个模块间的调用,所有的模块间的调用都会经过中间层中转。

但是发现增加这个中间层后,耦合还是存在的。中间层对被调用模块存在耦合,其他模块也需要耦合中间层才能发起调用。这样还是存在之前的相互耦合的问题,而且本质上比之前更麻烦了。

1.2 架构改进

所以应该做的是,只让其他模块对中间层产生耦合关系,中间层不对其他模块发生耦合。 对于这个问题,可以采用组件化的架构,将每个模块作为一个组件。并且建立一个主项目,这个主项目负责集成所有组件。这样带来的好处是很多的:

  • 业务划分更佳清晰,新人接手更佳容易,可以按组件分配开发任务。
  • 项目可维护性更强,提高开发效率。
  • 更好排查问题,某个组件出现问题,直接对组件进行处理。
  • 开发测试过程中,可以只编译自己那部分代码,不需要编译整个项目代码。
  • 方便集成,项目需要哪个模块直接通过CocoaPods集成即可。

进行组件化开发后,可以把每个组件当做一个独立的app,每个组件甚至可以采取不同的架构,例如分别使用MVVMMVCMVCS等架构,根据自己的编程习惯做选择。

二、蘑菇街组件化架构方案

2.1 MGJRouter方案

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

2.1.1 架构设计

MGJRouter组件化架构

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

MGJRouter是所有组件的调度中心,负责所有组件的调用、切换、特殊处理等操作,可以用来处理一切组件间发生的关系。除了原生页面的解析外,还可以根据URL跳转H5页面。

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

1
2
3
4
5
6
7
8
9
10
11
#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来集成,将所有组件当做二方库集成到项目中。详细的集成技术点在下面“标准组件化架构设计”章节中会讲到。

2.1.2 组件调用及传参

下面代码模拟对详情页的注册、调用,在调用过程中传递id参数。参数传递可以有两种方式,类似于Get请求URL后面拼接参数,以及通过字典传递参数。下面是注册的示例代码:

1
2
3
4
[MGJRouter registerURLPattern:@"mgj://detail" toHandler:^(NSDictionary *routerParameters) {
// 下面可以在拿到参数后,为其他组件提供对应的服务
NSString uid = routerParameters[@"id"];
}];

通过openURL:方法传入的URL参数,对详情页已经注册的block方法发起调用。调用方式类似于GET请求URL地址后面拼接参数。

1
[MGJRouter openURL:@"mgj://detail?id=404"];

也可以通过字典方式传参,MGJRouter提供了带有字典参数的方法,这样就可以传递非字符串之外的其他类型参数,例如对象类型参数。

1
[MGJRouter openURL:@"mgj://detail" withParam:@{@"id" : @"404"}];

2.1.3 组件调用数据回返

有的时候组件间调用过程中,需要服务方在完成调用后返回相应的参数。蘑菇街提供了另外的方法,专门来完成这个操作。

1
2
3
[MGJRouter registerURLPattern:@"mgj://cart/ordercount" toObjectHandler:^id(NSDictionary *routerParamters){
return @42;
}];

通过下面的方式发起调用,并获取服务方返回的返回值,要做的就是传递正确的URL和参数即可。

1
NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"];

2.1.4 短链管理

这时候会发现一个问题,在蘑菇街组件化架构中,存在了很多硬编码的URL和参数。在代码实现过程中URL编写出错会导致调用失败,而且参数是一个字典类型,调用方不知道服务方需要哪些参数,这些都是个问题。

对于这些数据的管理,蘑菇街开发了一个web页面,这个web页面统一来管理所有的URL和参数,AndroidiOS都使用这一套URL,可以保持统一性。

2.1.5 基础组件

在项目中存在很多公共部分的东西,例如封装的网络请求、缓存、数据处理等功能,以及项目中所用到的资源文件。蘑菇街将这些部分也当做组件,划分为基础组件,位于业务组件下层。所有业务组件都使用同一套基础组件,也可以保证公共部分的统一性。

2.2 Protocol方案

2.2.1 整体架构

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

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

在中间件中创建MGJComponentProtocol文件,服务方组件将可以用来调用的方法都定义在Protocol中,将所有服务方的Protocol都分别定义到MGJComponentProtocol文件中,如果协议比较多也可以分开几个文件定义。这样所有调用方依然是只依赖中间件,不需要依赖除中间件之外的其他组件。

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

Protocol方案依然需要提前注册服务,由于Protocol方案是返回一个Class,并将Class反射为对象再调用方法,这种方式不会直接调用类的内部逻辑。可以将Protocol方案的Class注册,都放在类对应的MGJModuleImplement中,或者专门建立一个RegisterProtocol类。

2.2.2 示例代码

创建MGJUserImpl类当做User组件对外公开的类,并在MGJComponentProtocol.h中定义MGJUserProtocol协议,由MGJUserImpl类实现协议中定义的方法,完成对外提供服务的过程。下面是协议定义:

1
2
3
@protocol MGJUserProtocol <NSObject>
- (NSString *)getUserName;
@end

Class遵守协议并实现定义的方法,外界通过Protocol获取的Class并实例化为对象,调用服务方实现的协议方法。

ModuleManager的协议注册方法,注册时将Protocol反射为字符串当做存储的key,将实现协议的Class当做值存储。通过ProtocolClass的时候,就是通过ProtocolModuleManager中将Class映射出来。

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

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

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

2.3 蘑菇街的结合使用

蘑菇街是 MGJRouter(openURL)Protocol 混用的方式:蘑菇街 App 的组件化之路·续《iOS 组件化方案探索》 — Bang

  • openURL 只是页面间的调用方式
  • 组件间的调用通过 protocol 来实现

当有一些场景不适合用 URL 的方式时,就可以通过注册 protocol 来实现:

  • 组件对外提供的接口主要是模块间代码层面上的调用,我们先称为本地调用,而 URL 主要用于 APP 间通信,姑且称为远程调用。蘑菇街 openUrl 方法中,将两者混在一起有什么问题呢?
  • 如果是 URL 的形式,那组件对外提供接口时就要同时考虑本地调用和远程调用两种情况,而远程调用有个限制,传递的参数类型有限制,只能传能被字符串化的数据,或者说只能传能被转成 json 的数据,像 UIImage 这类对象是不行的,所以如果组件接口要考虑远程调用,这里的参数就不能是这类非常规对象,接口的定义就受限了。
  • 用理论的话来说就是,远程调用是本地调用的子集,这里混在一起导致组件只能提供子集功能,所以这个方案是天生有缺陷的,对于遗漏的这部分功能,蘑菇街使用了 protocol-class 方案补全。

项目中 openURL 方案的使用流程:

  1. 在进入程序后,先使用MGJRouter对服务方组件进行注册。每个URL对应一个block的实现,block 中的代码就是组件对外提供的服务,调用方可以通过URL调用这个服务。
  2. 调用方通过MGJRouter调用openURL:方法,并将被调用代码对应的URL传入,MGJRouter会根据URL查找对应的block实现,从而调用组件的代码进行通信。
  3. 调用和注册block时,block有一个字典用来传递参数。这样的优势就是参数类型和数量理论上是不受限制的,但是需要很多硬编码的key名在项目中。

2.4 内存管理

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

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

  • 首先说一下MGJRouter方案可能导致的内存问题,由于block会对代码块内部对象进行持有,如果使用不当很容易造成内存泄漏的问题。 block自身实际上不会造成很大的内存泄漏,主要是内部引用的变量,所以在使用时就需要注意强引用的问题,并适当使用weak修饰对应的变量。以及在适当的时候,释放对应的变量。 除了对外部变量的引用,在block代码块内部尽量不要直接创建对象,应该通过方法调用中转一下。
  • 对于协议这种实现方式,和block内存常驻方式差不多。只是将存储的block对象换成Class对象。这实际上是存储的类对象,类对象本来就是单例模式,所以不会造成多余内存占用。

三、casatwy组件化架构方案

3.1 整体架构

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

  • 远程调用通过AppDelegate代理方法传递到当前应用后,调用远程接口并在内部做一些处理,处理完成后会在远程接口内部调用本地接口,以实现本地调用为远程调用服务
  • 本地调用由performTarget:action:params:方法负责,但调用方一般不直接调用performTarget:方法CTMediator会对外提供明确参数和方法名的方法,在方法内部调用performTarget:方法和参数的转换。

3.2 架构设计思路

casatwy是通过CTMediator类实现组件化的,在此类中对外提供明确参数类型的接口,接口内部通过performTarget方法调用服务方组件的TargetAction。由于CTMediator类的调用是通过runtime主动发现服务的,所以服务方对此类是完全解耦的。

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

对于服务方的组件来说,每个组件都提供一个或多个Target类,在Target类中声明Action方法。Target类是当前组件对外提供的一个“服务类”,Target将当前组件中所有的服务都定义在里面,CTMediator 通过 runtime 主动发现服务

Target中的所有Action方法,都只有一个字典参数,所以可以传递的参数很灵活,这也是casatwy提出的Model化的概念。在Action的方法实现中,对传进来的字典参数进行解析,再调用组件内部的类和方法。

3.3 架构分析

casatwy为我们提供了一个Demo,通过这个Demo可以很好的理解casatwy的设计思路,下面按照我的理解讲解一下这个Demo

打开Demo后可以看到文件目录非常清楚,在上图中用蓝框框出来的就是中间件部分,红框框出来的就是业务组件部分。我对每个文件夹做了一个简单的注释,包含了其在架构中的职责。

CTMediator中定义远程调用和本地调用的两个方法,其他业务相关的调用由Category完成。

1
2
3
4
// 远程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采取定义常量字符串来解决这个问题,这样管理也更方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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内部的,这样就可以保证组件内部的业务不受外部影响,对内部业务没有侵入性

1
2
3
4
5
6
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params {
// 对传过来的字典参数进行解析,并调用ModuleA内部的代码
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}

3.4 命名规范

在大型项目中代码量比较大,需要避免命名冲突的问题。对于这个问题casatwy采取的是加前缀的方式,从casatwyDemo中也可以看出,其组件ModuleATarget命名为Target_A,可以区分各个组件的Target。被调用的Action命名为Action_nativeFetchDetailViewController:,可以区分组件内的方法与对外提供的方法。

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

四、对比分析

Protocol

从我调研和使用的结果来说,并不推荐使用Protocol方案。首先Protocol方案的代码量就比MGJRouter方案的要多,调用和注册代码量很大,调用起来并不是很方便。

本质上来说Protocol方案是通过类对象实例一个变量,并调用变量的方法,并没有真正意义上的改变组件之间的交互方案,但MGJRouter的方案却通过URL Router的方式改变和统一了组件间调用方式。

并且Protocol没有对Remote Router的支持,不能直接处理来自Push的调用,在灵活性上就不如MGJRouter的方案。

CTMediator

我并不推荐CTMediator方案,这套方案实际上是一套很臃肿的方案。虽然为CTMediator提供了很多Category但实际上组件间的调用逻辑都耦合在了中间件中。同样,和Protocol方案存在一个相同的问题,就是调用代码量很大,使用起来并不方便。

CTMediator方案中存在很多硬编码的问题,例如targetaction以及参数名都是硬编码在中间件中的,这种调用方式并不灵活直接。

casatwy提出了去Model化的想法,我觉得这在组件化中传参来说,是非常灵活的,这点我比较认同。相对于MGJRouter的话,也采用了去Model化的传参方式,而不是直接传递模型对象。组件化传参并不适用传模型对象,但组件内部还是可以使用Model的。

MGJRouter

MGJRouter方案是一套非常轻量级的方案,其中间件代码总共也就两百行以内,非常简洁。在调用时直接通过URL调用,调用起来很简单,我推荐使用这套方案作为组件化架构的中间件。

MGJRouter最强大的一点在于,统一了远程调用和本地调用。这就使得可以通过Push的方式,进行任何允许的组件间调用,对项目运营是有很大帮助的。

这三套方案都实现了组件间的解耦,MGJRouterProtocol都是调用方对中间件的耦合,CTMediator是中间件对组件的耦合,都是单向耦合。

接口类

在三套方案中,服务方组件都对外提供一个PublicHeaderTarget在文件中统一定义对外提供的服务,组件间通信的实现代码大多数都在里面。

但三套实现方案实现方式并不同,蘑菇街的两套方案都需要注册操作,无论是Block还是Protocol都需要注册后才可以提供服务。而casatwy的方案则不需要,直接通过runtime调用。

五、组件化架构设计

在上面文章中提到了casatwy方案的CTMediator,蘑菇街方案的MGJRouterModuleManager,之后将统称为中间件,下面让我们设计一套组件化架构。

5.1 整体架构

组件化架构中,需要一个主工程,主工程负责集成所有组件。每个组件都是一个单独的工程,创建不同的git私有仓库来管理,每个组件都有对应的开发人员负责开发。开发人员只需要关注与其相关组件的代码,不用考虑其他组件,这样来新人也好上手。

组件的划分需要注意组件粒度,粒度根据业务可大可小。组件划分可以将每个业务模块都划分为组件,对于网络、数据库等基础模块,也应该划分到组件中。项目中会用到很多资源文件、配置文件等,也应该划分到对应的组件中,避免重复的资源文件。项目实现完全的组件化。

每个组件都需要对外提供调用,在对外公开的类或组件内部,注册对应的URL。组件处理中间件调用的代码应该对其他代码无侵入,只负责对传递过来的数据进行解析和组件内调用的功能。

5.2 组件集成

5.2.1 cocoapods管理

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

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

5.2.2 集成方式

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

无论是用CocoaPods管理源码,还是直接管理framework,集成方式都是一样的,都是直接进行pod updateCocoaPods操作。

这两种组件集成方案,实践中也是各有利弊。直接在主工程中集成代码文件,可以看到其内部实现源码,方便在主工程中进行调试。集成framework的方式,可以加快编译速度,而且对每个组件的代码有很好的保密性。如果公司对代码安全比较看重,可以考虑framework的形式。

例如手机QQ或者支付宝这样的大型程序,一般都会采取framework的形式。而且一般这样的大公司,都会有自己的组件库,这个组件库往往可以代表一个大的功能或业务组件,直接添加项目中就可以使用。关于组件化库在后面讲淘宝组件化架构的时候会提到。

5.2.3 资源文件

对于项目中图片的集成,可以把图片当做一个单独的组件,组件中只存在图片文件,没有任何代码。图片可以使用Bundleimage assets进行管理,如果是Bundle就针对不同业务模块建立不同的Bundle,如果是image assets,就按照不同的模块分类建立不同的assets,将所有资源放在同一个组件内。

Bundleimageassets两者相比,我还是更推荐用assets的方式,因为assets自身提供很多功能(例如设置图片拉伸范围),而且在打包之后图片会被打包在.cer文件中,不会被看到。(现在也可以通过工具对.cer文件进行解析,获取里面的图片)

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

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

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

1
pod 'Assets', :path => '../MainProject/Assets'  # 这种写法是访问本地的(Local Pods),也可以换成远程仓库,比如git)

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

5.3 优点

  • 组件化开发可以很好的提升代码复用性,组件可以直接拿到其他项目中使用, 这个优点在下面淘宝架构中会着重讲一下。
  • 对于调试工作,可以放在每个组件中完成。单独的业务组件可以直接提交给测试使用,这样测试起来也比较方便。最后组件开发完成并测试通过后,再将所有组件更新到主项目,提交给测试进行集成测试即可。
  • 通过这样的组件划分,组件的开发进度不会受其他业务的影响,可以多个组件并行开发。组件间的通信都交给中间件来进行,需要通信的类只需要接触中间件,而中间件不需要耦合其他组件,这就实现了组件间的解耦。中间件负责处理所有组件之间的调度,在所有组件之间起到控制核心的作用
  • 组件化框架清晰的划分了不同模块,从整体架构上来约束开发人员进行组件化开发,实现了组件间的物理隔离。组件化架构在各个模块之间天然形成了一道屏障,避免某个开发人员偷懒直接引用头文件,产生组件间的耦合,破坏整体架构。
  • 使用组件化架构进行开发时,因为每个人都负责自己的组件,代码提交也只提交自己负责模块的仓库,所以代码冲突的问题会变得很少
  • 假设以后某个业务发生大的改变,需要对相关代码进行重构,可以在单个组件内进行重构。组件化架构降低了重构的风险,保证了代码的健壮性。

5.4 架构分析

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

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

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

在调用MGJRouter时传入这个H5URL即可完成切换,MGJRouter判断如果传进来的是一个H5URL就直接跳转webView。而且URL可以传递参数给MGJRouter,只需要MGJRouter内部做参数截取即可。

使用组件化架构开发,组件间的通信都是有成本的。所以尽量将业务封装在组件内部,对外只提供简单的接口。即“高内聚、低耦合”原则

把握好组件划分粒度的细化程度,太细则项目过于分散,太大则项目组件臃肿。但是项目都是从小到大的一个发展过程,所以不断进行重构是掌握这个组件的细化程度最好的方式

5.5 注意点

如果通过framework等二进制形式,将组件集成到主项目中,需要注意预编译指令的使用。组件项目中预编译指令在其打包为framework的时候,就已经被预处理了。之后,framework就是固定的二进制数据,其不会再受主项目编译参数的影响。

六、我公司架构

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

6.1 架构设计

我公司项目是一个地图导航应用,业务层之下的核心模块和基础模块占比较大,涉及到地图SDK、算路、语音等模块。且基础模块相对比较独立,对外提供了很多调用接口。由此可以看出,公司项目是一个重逻辑的项目,不像电商等App偏展示。

项目整体的架构设计是:层级架构+组件化架构,对于具体的实现细节会在下面详细讲解。采取这种结构混合的方式进行整体架构,对于组件的管理和层级划分比较有利,符合公司业务需求。

在设计架构时,我们将整个项目都拆分为组件,组件化程度相当高。用到哪个组件就在工程中通过Podfile进行集成,并通过URLRouter统一所有组件间的通信。

组件化架构是项目的整体框架,而对于框架中每个业务模块的实现,可以是任意方式的架构,MVVMMVCMVCS等都是可以的,只要通过MGJRouter将组件间的通信方式统一即可。

6.2 分层架构

组件化架构在物理结构上来说是不分层次的,只有组件与组件之间的划分关系。但是在组件化架构的基础上,应该根据项目和业务设计自己的层次架构,这套层次架构可以用来区分组件所处的层次及职责,所以我们设计了层级架构+组件化架构的整体架构。

6.2.1 层次的设计

我公司项目最开始设计的是三层架构:业务层 -> 核心层 (high + low) -> 基础层,其中核心层又分为highlow两部分。但是这种架构会造成核心层过重,基础层过轻的问题,这种并不适合组件化架构。

在三层架构中会发现,low层并没有耦合业务逻辑,在同层级中是比较独立的,职责较为单一和基础。我们对low层下沉到基础层中,并和基础层进行合并。所以架构被重新分为三层架构:业务层 -> 核心层 -> 基础层。之前基础层大多是资源文件和配置文件,在项目中存在感并不高。

在分层架构中,需要注意只能上层对下层依赖,下层对上层不能有依赖,下层中不要包含上层业务逻辑。对于项目中存在的公共资源和代码,应该将其下沉到下层中。

6.2.2 职责划分

在三层架构中:

  • 业务层负责处理上层业务,将不同业务划分到相应组件中。例如IM组件、导航组件、用户组件等。
    • 业务层的组件间关系比较复杂,会涉及到组件间业务的通信,以及业务层组件对下层组件的引用。
  • 核心层位于业务层下方,为业务层提供业务支持。例如网络、语音识别等组件应该划分到核心层。
    • 核心层应该尽量减少组件间的依赖,将依赖降到最小。
    • 核心层有时相互之间也需要支持,例如经纬度组件需要网络组件提供网络请求的支持,这种是不可避免的。
  • 其他比较基础的模块,都放在基础层当做基础组件。例如AFN、地图SDK、加密算法等。
    • 这些组件都比较独立且不掺杂任何业务逻辑,职责更加单一,相对于核心层更底层。
    • 可以包含第三方库、资源文件、配置文件、基础库等几大类,基础层组件相互之间不应该产生任何依赖。

对于核心层和基础层的划分,可以以是否涉及业务、是否涉及同级组件间通信、是否经常改动为参照点。如果符合这几点则放在核心层,如果不符合则放在基础层。

在设计各个组件时,应该遵循“高内聚,低耦合”的设计规范,组件的调用应该简单且直接,减少调用方的其他处理。

6.2.3 集成方式

新建一个项目后,首先将配置文件、URLRouterApp容器等集成到主工程中,做一些基础的项目配置,随后集成需要的组件即可。

项目被整体拆分为组件化架构后,应用对所有组件的集成方式都是一样的,通过Podfile将需要的组件集成到项目中。通过组件化的方式,使得开发新项目速度变得非常快。

  • 业务层和核心层组件集成后,组件间的通信都是由URLRouter进行通信,项目中不允许直接依赖组件源码。
  • 基础层组件在集成后则是直接依赖,例如:
    • 资源文件和配置文件,这些都是直接在主工程或组件中使用的。
    • 核心层也是直接依赖第三方库源码的,第三方库则是通过核心层的业务封装,封装后由URLRouter进行通信。

组件的集成方式有两种,源码和framework的形式,我们使用framework的方式集成。因为一般都是项目比较大才用组件化的,但大型项目都会存在编译时间的问题,如果通过framework则会大大减少编译时间,可以节省开发人员的时间。

6.3 组件间通信

对于组件间通信,我们采用的MGJRouter方案。因为MGJRouter现在已经很稳定了,而且可以满足蘑菇街这样量级的App需求,证明是很好的,没必要自己写一套再慢慢踩坑。

MGJRouter的好处在于,其调用方式很灵活,通过MGJRouter注册并在block中处理回调,通过URL直接调用或者URL+Params字典的方式进行调用。由于通过URL拼接参数或Params字典传值,所以其参数类型没有数量限定,传递比较灵活。在通过openURL:调用后,可以在completionBlock中处理完成逻辑。

6.3.1 路由表

MGJRouter有个问题在于,在编写组件间通信的代码时,会涉及到大量的Hardcode。对于Hardcode的问题,蘑菇街开发了一套后台系统,将所有的Router需要的URL和参数名,都定义到这套系统中。我们维护了一个Plist表,内部按不同组件进行划分,包含URL和传参名以及回调参数。

组件Router表

6.3.2 路由层安全

组件化架构需要注意路由层的安全问题。MGJRouter方案可以处理本地及远程的OpenURL调用,如果是程序内组件间的OpenURL调用,则不需要进行校验。而跨应用的OpenURL调用,则需要进行合法性检查。这是为了防止第三方伪造进行OpenURL调用,所以对应用外调起的OpenURL进行的合法性检查,例如其他应用调起、服务器Remote Push等。

在合法性检查的设计上,每个从应用外调起的合法URL都会带有一个token,在本地会对token进行校验。这种方式的优势在于,没有网络请求的限制和延时。

6.3.3 代理方法

在项目中经常会用到代理模式传值,代理模式在iOS中主要分为三部分,协议、代理方、委托方三部分。

代理设计模式

但如果使用组件化架构的话,会涉及到组件与组件间的代理传值,代理方需要设置为委托方的delegate,但组件间是不可以直接产生耦合的。对于这种跨组件的代理情况,我们直接将代理方的对象通过MGJRouter以参数的形式传给另一个组件,在另一个组件中进行代理设置。

1
2
3
4
5
6
7
8
9
HomeViewController *homeVC = [[HomeViewController alloc] init];
NSDictionary *params = @{CTBUserCenterLoginDelegateKey : homeVC};
[MGJRouter openURL:@"CTB://UserCenter/UserLogin" withUserInfo:params completion:nil];

[MGJRouter registerURLPattern:@"CTB://UserCenter/UserLogin" toHandler:^(NSDictionary *routerParameters) {
UIViewController *homeVC = routerParameters[CTBUserCenterLoginDelegateKey];
LoginViewController *loginVC = [[LoginViewController alloc] init];
loginVC.delegate = homeVC;
}];

协议的定义放在委托方组件的PublicHeader.h中,代理方组件只引用这个PublicHeader.h文件,不耦合委托方内部代码。为了避免定义的代理方法中出现耦合的情况,方法中不能出现和组件内部业务有关的对象,只能传递系统的类。如果涉及到交互的情况,则通过协议方法的返回值进行。

6.3.4 组件调用传参

MGJRouter可以在openURL:时传入一个NSDictionary参数,在接触RAC之后,我在想是不是可以把NSDictionary参数变为RACSignal参数,直接传一个信号过去

注册MGJRouter

1
2
3
4
5
6
7
8
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"刘小壮"];
return [RACDisposable disposableWithBlock:^{
NSLog(@"disposable");
}];
}];

[MGJRouter registerURLPattern:@"CTB://UserCenter/getUserInfo" withSignal:signal];

调用MGJRouter

1
2
3
4
RACSignal *signal = [MGJRouter openURL:@"CTB://UserCenter/getUserInfo"];
[signal subscribeNext:^(NSString *userName) {
NSLog(@"userName %@", userName);
}];

这种方式是可行的。使用RACSignal方式优点在于,相对于直接传字典过去更加灵活,并且具备RAC的诸多特性。但缺点也不少,信号控制不好乱用的话也很容易挖坑,是否使用还是看团队情况了。

6.3.5 常量定义

在项目中经常会定义一些常量,例如通知名、常量字符串等,这些常量一般都和所属组件有很强的关系,不好单独拆出来放到其他组件。但是这些变量数量并不是很多,而且不是每个组件中都有。

所以,我们将这些变量都声明在PublicHeader.h文件中,其他组件只能引用PublicHeader.h文件,不能引用组件内部业务代码,这样就规避掉了组件间耦合的问题。

6.4 H5和Native通信

在项目中经常会用到H5页面,如果能通过点击H5页面调起原生页面,这样的话NativeH5的融合会更好。所以我们设计了一套H5Native交互的方案,这套方案可以使用URLRouter的方式调起原生页面,实现方式也很简单,并且这套方案和H5原本的跳转逻辑并不冲突。

通过iOS自带UIWebView创建一个H5页面后,H5可以通过调用下面的JS函数和Native通信。调用时可以传入新的URL,这个URL可以设置为URLRouterURL

1
window.location.href = 'CTB://UserCenter/UserLogin?userName=lxz&WeChatID=lz2046703959';

通过JS刷新H5页面时,会调用下面的代理方法。

1
2
// 如果方法返回YES,则会根据URL协议进行跳转。
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

跳转时系统会判断通信协议:

  • 如果是HTTP等标准协议,则会在当前页面进行刷新。
  • 如果跳转协议在URL Schame中注册,则会通过系统openURL:的方式调用到AppDelegate的系统代理方法中,在代理方法中调用URLRouter,则可以通过H5页面唤起原生页面。

6.5 AppService

在应用启动过程中,通常会做一些初始化操作。有些初始化操作是运行程序所需要的,例如崩溃统计、建立服务器的长连接等。或有的组件会对初始化操作有依赖关系,例如网络组件依赖requestToken等。

对于应用启动时的初始化操作,应该创建一个AppService来统一管理启动操作,将初始化操作都放在里面,包含创建根控制器等。其中有的初始化操作需要尽快执行,有的并不需要立即执行,可以根据不同操作设定优先级,来管理所有初始化操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <Foundation/Foundation.h>

typedef NS_ENUM(NSUInteger, CTBAppServicePriority) {
CTBAppServicePriorityLow,
CTBAppServicePriorityDefault,
CTBAppServicePriorityHigh,
};

@interface CTBAppService : NSObject
+ (instancetype)appService;
- (void)registerService:(dispatch_block_t)serviceBlock
priority:(CTBAppServicePriority)priority;
@end

6.6 Model层设计

项目中存在很多的模型定义,那组件化后这些模型应该定义在哪呢?

casatwy对模型类的观点是去Model化,简单来说就是用字典代替Model存储数据。这对于组件化架构来说,是解决组件之间数据传递的一个很好的方法。但是去Model的方式,会存在大量的字段读取代码,使用起来远没有模型类方便。

因为模型类是关乎业务的,理论上必须放在业务层也就是业务组件这一层。但是要把模型对象从一个组件中当做参数传递到另一个组件中,模型类放在调用方和被调方的哪个组件都不太合适,而且有可能不只两个组件使用到这个模型对象。这样的话在其他组件使用模型对象,必然会造成引用和耦合

如果在用到这个模型对象的所有组件中,都分别维护一份相同的模型类,或者各自维护不同结构的模型类,这样之后业务发生改变模型类就会很麻烦,这是不可取的。

设计方案

如果将所有模型类单独拉出来,定义一个模型组件呢?

这个看起来比较可行,将这个定义模型的组件下沉到基础层,模型组件不包含业务,只声明模型对象的类。 如果将原来各个组件的模型类定义都拉出来,单独放在一个组件中,可以将原有各组件的Model层变得很轻量,这样对整个项目架构来说也是有好处的。

  • 在通过Router进行组件间调用时,通过字典进行传值,这种方式比较灵活。
  • 在组件内部使用Model层时,还是用模型组件中定义的Model类。Model层建议还是用Model对象的形式比较方便,不建议整体使用去Model化的设计。
  • 在接收到其他组件传递过来的字典参数时,可以通过Model类提供的初始化方法,或其他转Model框架将字典转为Model对象。
1
2
3
4
5
6
@interface CTBStoreWelfareListModel : NSObject
/**
* 自定义初始化方法
*/
- (instancetype)initWithDict:(NSDictionary *)dict;
@end

我公司持久化方案用的是CoreData,所有模型的定义都在CoreData组件中,则不需要再单独创建一个模型组件。

6.7 动态化构想

我公司项目是一个常规的地图类项目,首页和百度、高德等主流地图导航App一样,有很多添加在地图上的控件。有的版本会添加控件上去,而有的版本会删除控件,与之对应的功能也会被隐藏。

所以,有次和组里小伙伴们开会的时候就在考虑, 能不能在服务器下发代码对首页进行布局! 这样就可以对首页进行动态布局,例如有活动的时候在指定时间显示某个控件,这样可以避免App Store审核慢的问题。又或者线上某个模块出现问题,可以紧急下架出问题的模块。

对于这个问题,我们设计了一套动态配置方案,这套方案可以对整个App进行配置。

配置表设计

对于动态配置的问题,我们简单设计了一个配置表,初期打算在首页上先进行试水,以后可能会布置到更多的页面上。这样应用程序各模块的入口,都可以通过配置表来控制,并且通过Router控制页面间跳转,灵活性非常大。

在第一次安装程序时使用内置的配置表,之后每次都用服务器来替换本地的配置表,这样就可以实现动态配置应用。下面是一个简单设计的配置数据,JSON中配置的是首页的配置信息,用来模拟服务器下发的数据,真正服务器下发的字段会比这个多很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"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。在此基础上设计缓存机制,定期清理本地的图片缓存,减少用户磁盘占用。

七、滴滴组件化架构

之前看过滴滴iOS负责人李贤辉的滴滴出行iOS客户端架构演进之路,分享的是滴滴iOS客户端的架构发展历程,下面简单总结一下。

7.1 发展历程

滴滴在最开始的时候架构较混乱。然后在2.0时期重构为MVC架构,使项目划分更加清晰。在3.0时期上线了新的业务线,这时开始采用游戏开发中的状态机机制,暂时可以满足现有业务。

然而在后期不断上线顺风车、代驾、巴士等多条业务线的情况下,现有架构变得非常臃肿代码耦合严重。从而在2015年开始了代号为“The One”的方案,这套方案就是滴滴的组件化方案。

7.2 架构设计

滴滴的组件化方案,和蘑菇街方案类似,将项目拆分为各个组件,通过CocoaPods来集成和管理各个组件。项目被拆分为业务部分和技术部分,业务部分包括专车、拼车、巴士等组件,使用一个pods管理。技术部分则分为登录分享、网络、缓存这样的一些基础组件,分别使用不同的pods管理。

组件间通信通过ONERouter中间件进行通信,ONERouter类似于MGJRouter担负起协调和调用各个组件的作用。组件间通信通过OpenURL方法,来进行对应的调用。ONERouter内部保存一份Class-URL的映射表,通过URL找到Class并发起调用,Class的注册放在+load方法中进行。

滴滴在业务组件内部使用MVVM+MVCS混合的架构,两种架构都是MVC的衍生版本。其中MVCS中的Store负责数据相关逻辑,例如订单状态、地址管理等数据处理。通过MVVM中的VM给控制器瘦身,最后Controller的代码量就很少了。

7.3 滴滴首页分析

滴滴文章中说道首页只能有一个地图实例,这在很多地图导航相关应用中都是这样做的。滴滴首页主控制器持有导航栏和地图,每个业务线首页控制器都添加在主控制器上,并且业务线控制器背景都设置为透明,将透明部分响应事件传递到下面的地图中,只响应属于自己的响应事件。

由主控制器来切换各个业务线首页,切换页面后根据不同的业务线来更新地图数据

八、淘宝组件化架构

本章节源自于宗心在阿里技术沙龙上的一次分享:手机淘宝客户端架构探索实践

8.1 架构发展

淘宝iOS客户端初期是单工程的普通项目,但随着业务的飞速发展,现有架构并不能承载越来越多的业务需求,导致代码间耦合很严重。后期开发团队对其不断进行重构,将项目重构为组件化架构,淘宝iOSAndroid两个平台,除了某个平台特有的一些特性或某些方案不便实施之外,大体架构都是差不多的。

发展历程

  1. 刚开始是普通的单工程项目,以传统的MVC架构进行开发。随着业务不断的增加,导致项目非常臃肿、耦合严重。
  2. 2013年淘宝开启 “all in 无线”计划 ,计划将淘宝变为一个大的平台,将阿里系大多数业务都集成到这个平台上,造成了业务的大爆发。 淘宝开始实行插件化架构,将每个业务模块划分为一个子工程,将组件以framework二方库的形式集成到主工程。但这种方式并没有做到真正的拆分,还是在一个工程中使用git进行merge,这样还会造成合并冲突、不好回退等问题。
  3. 迎来淘宝移动端有史以来最大的重构,将其重构为组件化架构。将每个模块当做一个组件,每个组件都是一个单独的项目,并且将组件打包成framework。主工程通过podfile集成所有组件的framework,实现业务之间真正的隔离,通过CocoaPods实现组件化架构。

8.2 架构优势

淘宝是使用git来做源码管理的,在插件化架构时需要尽可能避免merge操作,否则在大团队中协作成本是很大的。而使用CocoaPods进行组件化开发,则避免了这个问题。

CocoaPods中可以通过podfile很好的配置各个组件,包括组件的增加和删除,以及控制某个组件的版本。使用CocoaPods的原因,很大程度是为了解决大型项目中,代码管理工具merge代码导致的冲突。并且可以通过配置podfile文件,轻松配置项目。

每个组件工程有两个target一个负责编译当前组件和运行调试,另一个负责打包 framework。先在组件工程做测试,测试完成后再集成到主工程中集成测试。

每个组件都是一个独立app,可以独立开发、测试,使得业务组件更加独立,所有组件可以并行开发。下层为上层提供能满足需求的底层库,保证上层业务层可以正常开发,并将底层库封装成framework集成到主工程中。

使用CocoaPods进行组件集成的好处在于,在集成测试自己组件时,可以直接在本地主工程中,通过podfile使用当前组件源码,可以直接进行集成测试,不需要提交到服务器仓库。

8.3 淘宝四层架构

淘宝架构的核心思想是一切皆组件,将工程中所有代码都抽象为组件。

淘宝架构主要分为四层,最上层是组件Bundle(业务组件),依次往下是容器(核心层),中间件Bundle(功能封装),基础库Bundle(底层库)。容器层为整个架构的核心,负责组件间的调度和消息派发。

8.4 总线设计

总线设计:URL路由+服务+消息。统一所有组件的通信标准,各个业务间通过总线进行通信。

8.4.1 URL总线

通过URL总线对三端进行了统一,一个URL可以调起iOSAndroid、前端三个平台,产品运营和服务器只需要下发一套URL即可调用对应的组件。

URL路由可以发起请求也可以接受返回值,和MGJRouter差不多。URL路由请求可以被解析就直接拿来使用,如果不能被解析就跳转H5页面。这样就完成了一个对不存在组件调用的兼容,使用户手中比较老的版本依然可以显示新的组件。

服务提供一些公共服务,由服务方组件负责实现,通过Protocol进行调用。

8.4.2 消息总线

应用通过消息总线进行事件的中心分发,类似于iOS的通知机制。例如客户端前后台切换,则可以通过消息总线分发到接收消息的组件。因为通过URLRouter只是一对一的进行消息派发和调度,如果多次注册同一个URL,则会被覆盖掉。

8.5 Bundle App

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

九、总结

9.1 留个小思考

下面留个小思考,把下面字符串复制到微信输入框随便发给一个好友,然后点击下面链接大概也能猜到微信的组件化方案

1
weixin://dl/profile

9.2 Demo

Author:Tenloy

原文链接:https://tenloy.github.io/2021/04/29/iOS-ComRouter.html

发表日期:2021.04.29 , 10:09 AM

更新日期:2024.04.07 , 8:02 PM

版权声明:本文采用Crative Commons 4.0 许可协议进行许可

CATALOG
  1. 一、组件化架构的由来
    1. 1.1 原因
    2. 1.2 架构改进
  2. 二、蘑菇街组件化架构方案
    1. 2.1 MGJRouter方案
      1. 2.1.1 架构设计
      2. 2.1.2 组件调用及传参
      3. 2.1.3 组件调用数据回返
      4. 2.1.4 短链管理
      5. 2.1.5 基础组件
    2. 2.2 Protocol方案
      1. 2.2.1 整体架构
      2. 2.2.2 示例代码
    3. 2.3 蘑菇街的结合使用
    4. 2.4 内存管理
  3. 三、casatwy组件化架构方案
    1. 3.1 整体架构
    2. 3.2 架构设计思路
    3. 3.3 架构分析
    4. 3.4 命名规范
  4. 四、对比分析
    1. Protocol
    2. CTMediator
    3. MGJRouter
    4. 接口类
  5. 五、组件化架构设计
    1. 5.1 整体架构
    2. 5.2 组件集成
      1. 5.2.1 cocoapods管理
      2. 5.2.2 集成方式
      3. 5.2.3 资源文件
    3. 5.3 优点
    4. 5.4 架构分析
    5. 5.5 注意点
  6. 六、我公司架构
    1. 6.1 架构设计
    2. 6.2 分层架构
      1. 6.2.1 层次的设计
      2. 6.2.2 职责划分
      3. 6.2.3 集成方式
    3. 6.3 组件间通信
      1. 6.3.1 路由表
      2. 6.3.2 路由层安全
      3. 6.3.3 代理方法
      4. 6.3.4 组件调用传参
      5. 6.3.5 常量定义
    4. 6.4 H5和Native通信
    5. 6.5 AppService
    6. 6.6 Model层设计
      1. 设计方案
    7. 6.7 动态化构想
      1. 配置表设计
      2. 资源动态配置
  7. 七、滴滴组件化架构
    1. 7.1 发展历程
    2. 7.2 架构设计
    3. 7.3 滴滴首页分析
  8. 八、淘宝组件化架构
    1. 8.1 架构发展
    2. 8.2 架构优势
    3. 8.3 淘宝四层架构
    4. 8.4 总线设计
      1. 8.4.1 URL总线
      2. 8.4.2 消息总线
    5. 8.5 Bundle App
  9. 九、总结
    1. 9.1 留个小思考
    2. 9.2 Demo