组件化架构漫谈

作者: 刘小壮 | 来源:发表于2016-09-17 16:56 被阅读59115次
    该文章属于<简书 — 刘小壮>原创,转载请注明:

    <简书 — 刘小壮> http://www.jianshu.com/p/67a6004f6930


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

    在学习组件化架构的过程中,从很多高质量的博客中学到不少东西,例如蘑菇街李忠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😁。

    组件化架构集成Demo


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

    如果你觉得不错,请把PDF帮忙转到其他群里,或者你的朋友,让更多的人了解组件化架构,衷心感谢!😁

    组件化架构PDF

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

    相关文章

      网友评论

      • 黄花菜先生:请教个问题,业务层有个模块有一个常量,别的模块如果要用,应该怎么处理?举个例子,登录模块有一个登录成功的通知,
        FOUNDATION_EXPORT NSString * const KLLoginSuccessNotification;我在别的业务模块怎么才能拿到这个通知,谢谢
      • 黄花菜先生:哥们,你们埋点是怎么做的,能说说嘛
        刘小壮:手写埋点,没有采取无痕埋点方案。
      • 云图平台:兄弟,有个问题想请教一下,我们的项目也用的是组件化方案,但是在某个模块(比如音视频模块),它会单独弄成一个模块组件,其内部我们想让它自己维护自己的coredata,在podSpec中使用
        s.resource_bundles = {
        'TZFileDownloader' => ['TZFileDownloaderTool/OtherResource/*'] #xcdatamodel放在OtherResource文件夹中
        }
        来将coredata的xcdatamodel资源文件加入项目中.
        在开发过程中没有问题,直到我们产品发布,上传appstore抱如下错误:
        ERROR ITMS-90171: "Invalid Bundle Structure - The binary file 'xxxxx.app/Frameworks/TZFileDownloaderTool.framework/OtherRes.bundle/TZFileDownloaderTool +CoreDataModel.o' is not permitted. Your app can’t contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. Refer to the Bundle Programming Guide at https://developer.apple.com/go/?id=bundle-structure for information on the iOS app bundle structure."

        然后就悲剧了,组件里面使用coredata会抱着个错误.
        最后没办法,只能临时将coredata资源文件放到主工程中,这样下来的话,模块化方案就没办法继续进行下去了.
        请兄弟指教一下,是不是组件内使用coredata的姿势不对?还是方案不对呢?
        云图平台:另外,我们的是swift项目,不知是否对这样组件化的方式有影响.
      • PPAbner:相见恨晚。码字真的不容易,关键是码出来的还是这么好的文章,赞一个。说实话,关于架构的文章,看过很多,没一个认真看完的,只有你这个。
        PPAbner:@刘小壮 :stuck_out_tongue_winking_eye::smile::smile::smile:
        刘小壮:如此高的评价,让我受宠若惊啊:smile:
      • SuperBoy_Timmy:非常nice,手动点赞~
        SuperBoy_Timmy:@刘小壮 666,有时间一定好好看看:smile:
        刘小壮:我Github上,还有更nice的东西等着你:wink:
      • 夏趣意转秋来:你们是怎样分工的呢? 公共组件是大家一起维护还是由某个人单独维护的呢?
        刘小壮:某个人维护,比如谁负责网络库组件、谁负责数据库组件之类的。公共组件一般很少发生改变,主要变化都是业务组件。
      • 黄花菜先生:各个开发环境的切换,你们是怎么处理的,单独写了一个环境切换的私有库?
        刘小壮:@不会凉的黄花菜 我们写的比较简单,直接预编译指令控制的。
        黄花菜先生:@刘小壮 是的
        刘小壮:debug和release这样的?
      • 157a50dc2555:赞赞赞
      • 黄花菜先生:为什么你们选了蘑菇街的方案?如果我要组件化我们的项目,蘑菇街和casatwy的方案如何选择?
        刘小壮:@不会凉的黄花菜 蘑菇街方案只需要维护一张注册表,注册表的key都定义成宏就行,这个比较简单。但是casatwy维护的硬编码就多了。
        黄花菜先生:@刘小壮 很容易出问题?能具体点吗?除了硬编码那一块,还有什么地方容易出问题?而且我个人认为维护一个注册表也很麻烦,我看casatwy的文章,提出了蘑菇街方案的很多弊端
        刘小壮:casatwy的方案用的是runtime实现,然后各个组件各种定义target、action的字符串,这种很容易出问题。蘑菇街相对而言就简单很多,统一定义URL然后映射就可以。
      • 蓝蓝的白云:写的真好,好详细
      • 黄花菜先生:常量,各个环境的域名,还有一些常用宏,放在哪?
        刘小壮:@不会凉的黄花菜 公共部分要单独做一个组件。测试环境、正式环境域名或者IP写在网络模块里。
        黄花菜先生:@刘小壮 公共的定义是要单独做一个组件吗?还有比如说测试环境,正式环境,预发布环境的域名的宏要写在那?写在项目中,还是写在网络组件中?
        刘小壮:常量放在各个模块里,比如登录完成发通知之类的,哪个模块用到就直接引用。
        一些项目里公共的定义,要放在统一的地方定义,比如说获取screen的width等。
      • 开飞机的叔客:强,学习了!
      • 电动鸡翅:受益匪浅,受益匪浅。有个问题想讨论一下,对于一个app的model层,如果是抽出一个组件。假如另一个app需要该app的某项业务组件,这个组件必然依赖刚才那个model组件,pod该业务组件同时也就要pod该model组件,而model组件还有很多其它业务组件的model,很恶心啊。所以model跟着产生该model数据的组件放一块不是更好嚒?耦合度更低呢
        刘小壮:如果用传统的继承自NSObject的那种模型,建议放在一起定义,这种就能避免相互嵌套的问题。模型之间相互嵌套,也是在同组件内部的。
        刘小壮:我们当时Model选型是CoreData,这个避免了模型划分的问题,本来就都在一个CoreData模块。
        刘小壮:是的,这个问题特别恶心。当时做组件化的时候也在想这个问题,模型到底放哪合适。因为有的模型好多地方都在用,有的模型里面还嵌套模型,这就不好处理了。
      • 8c943ff6c7c8:读完受益匪浅。
        我想问一下,工程的图片、声音、本地化的字符串等资源文件,是按照业务划分到各个组件之内,还是所有资源整合成单独的组件?如果是整合到一个组件内,业务A只用了两张图,集成整个组件显得冗余,查找起来也不方便。如果分散到各个业务组件又不能统一管理。想了解一下你的看法。
        刘小壮:我们是把所有资源文件整合到一个组件里。

        以图片为例,如果A模块需要用到一张图片,B模块里恰巧有,这时候直接用就可以,从资源组件就可以找到这张图片。

        如果划分到各自的组件里的话,在实际开发中经常是哪里用到就拖到哪个项目里,很容易出现重复资源。
      • 29139575bc22:兄弟,想问一下你们api是怎么来划分的呢?我们公司整个项目的api是都放在网络层的,最近也在想如果将整个api搞成一个组件,那么每个业务模块都要去修改这同一个组件,比较麻烦,第二是不同的api可能会有些自定义的东西,比如有的可以分页加载,或者加载时间可以长一点等等。不知道你们有没有什么好的方案呢?
        刘小壮:@Tyrone_02ab 我们采用“集约型”网络架构,也就是定义基础请求类,新的请求基于这个类创建子类的那种。把基础部分和公共参数这些通用的东西,放在单独的网络模块,具体的网络请求实现放在上层,上层代码量也很简单,直接集成一个子类传一个URL就可以用。
        29139575bc22:@刘小壮 api就是数据访问接口,就比如获取用户数据,获取列表数据,获取任何网络数据这种网络层。
        刘小壮:你说的API指的是?
      • l富文本l:看你的博客学习到了很多,谢谢
        刘小壮:多谢认可,共同努力!:smile:
      • louiszgm:你好:
        我在主工程中pod ModulaA,假如ModulaA中的.pch文件通过s.prefix_header_file 导入了。
        那么我在主工程中能够使用ModulaA的pch吗?
        刘小壮:可以,对Spec配置好就行。
      • 刘小壮:https://github.com/DeveloperErenLiu/ComponentArchitectureBook

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

        如果你觉得不错,请把PDF帮忙转到其他群里,或者你的朋友,让更多的人了解组件化架构,衷心感谢!😁
      • 低吟浅唱1990:在+load方法中注册一个模块需要用到的URL,会不会出现一些莫名奇妙的问题。比如类的加载顺序不确定导致的问题等
        刘小壮:@低吟浅唱1990 其实问题不大,一般没什么问题。block只是对内部对象做了一个拷贝,等在未来某个时机才会执行,那时候一般Class已经初始化完毕了。
        低吟浅唱1990:@刘小壮 是的。 如果组件很多,一开始要加载注册很多的东西的
        刘小壮:你是指执行+load方法时,这个类并没有“准备好”,有些东西还没有初始化好,是吗?
      • 吴欧:牛逼牛逼 都是大佬!
        吴欧:@刘小壮 最好附一个简单的demo呀,看文章都看晕了,我也想玩玩组件化:kissing_heart: :kissing_heart:
        刘小壮:过了一年多,感觉之前的设计方案有点问题,做了些架构优化。
        刘小壮:过两天我更新文章,再过来看看:blush:
      • 8bfa4aaf88e2:感谢大神,学到很多。。。。大赞!!!
      • 我的大名叫小爱:详细看了文章 还没动手实践 准备动手实践下。
        刘小壮:嗯,实践出真章
      • Mr_0:好好好
      • roylly:好文儿,看大家的回复就知道了
        刘小壮:毕竟有好多是真正钻研技术的,大家一起在评论区讨论技术,氛围挺好。
        刘小壮:多谢认可。
      • b57bc247097e:先赞一个。我们现在准备把公司旗下所有移动端全部整合到一起。 就可能登陆进来首页 然后下面提供几个入口进入到不同的项目中。适合用这种组件化吗
        刘小壮:@咖啡铯眼涙 可以的。这样这几个App共同的模块,也可以统一在一起了。
        b57bc247097e:@刘小壮 是的 想把几个app全部做到一个平台上
        刘小壮:不同的项目?不同的APP吗
      • 我爱吃大糖饼:受益匪浅,楼主辛苦,必须点star啊,得慢慢消化,打算有时间再看一遍。
        刘小壮:共同进步
      • winvsmary:github使用私有仓库是需要收费的,每月7刀,那你们公司项目不可能用github做仓库吧!还是仅仅只用来组件化
        刘小壮:@_会飞的鱼 对,用的路由,和MGJRouter的差不多。
        _会飞的鱼:模块组件化之后 你们的中间件是路由么
        刘小壮:。。。干嘛要用Github的,自己公司搭一个git服务器,不就是私有仓库吗。
      • 05497d31370f:只有我想知道展位图是哪里么... 那个金字塔形的建筑是什么... ?
        刘小壮:你不是第一个问占位图的,其他人也有,看来这些图片挺受欢迎。
        刘小壮:我也不知道,从其他图片网站下载的:joy:
      • SamCheck:为了给你点赞 都把我遗忘的密码给找回来了
        刘小壮:太感谢了:smile:
      • 大雄君噢:如果是有tabbar的 最好的方式要怎么处理?
        刘小壮:@大雄君噢 最近忙,等过段时间再继续写吧:sob:
        大雄君噢:@刘小壮 最近没更新博客了呢 ?
        刘小壮:tabbar还是放在主工程里吧,这样最合适。
      • 进击的小短腿:总结的很好:+1:
      • 醉卧栏杆听雨声:赞!有个疑问,就是在开发业务组件的时候怎样剥离业务组件对基础组件的耦合?因为在开发过程中,必须依赖一些基础组件来测试,测试完成后就移除基础组件么?
        刘小壮:@狂人日记_wd 我们的基础组件是单独放在一个私有仓库的,如果没更新就不用提交。
        醉卧栏杆听雨声:@刘小壮 git提交的时候直接不提交基础组件吧
        刘小壮:基础组件也放在私有仓库,用CocoaPods一起集成,和业务组件一样就行。
      • o0下一站生活0o:用framework不是不能上架吗?这点没搞懂。
        刘小壮:@我的大名叫小爱 对,是的
        我的大名叫小爱:@刘小壮 动态framework可以上架
        刘小壮:之前听说过自己创建的动态库上架会被拒,但是从后来苹果推出自己的动态库之后,好像就没这个问题了。
      • Yanqilong:请教一个问题,我项目中请求接口前先要获取代理服务器,组件化之后,我这获取代理服务器的代码要放在哪里,要怎么架构呢?
        刘小壮:项目中应该有一个网络模块,这部分应该放在网络模块中。
      • 刘小壮:当前文章已经更新Demo,主要是写组件化架构集成的,其他思想上的东西没写。具体在文章最后面,有详细的描述。
      • 会跳舞的狮子: 很喜欢你的文章, 虽然看起来萌萌的 , 但还是受益匪浅
        刘小壮:@会跳舞的狮子 我最近简单写了个组件化集成的Demo,写的很简单,放在我Github上了。地址更新到文章中了。
      • End_枫:先谢谢您的分享,然后看完了有个问题,比如登陆模块是一个组件,网络模块是一个组件。还有MGJRouter中间件。那么登陆模块单独的工程里是否得包涵进 网络模块和中间件,否则登陆组件如何单独的run起来测试呢。
        我的理解是,每个单独的组件A工程里,都包含了必要的 依赖组件,这样才可以做单独测试,缺了必要的组件,组件A就编译不起来,不知道我的理解对不对。

        本来的我的想法是每个单独的组件不需要依赖任何组件就可以编译运行,虽然结果功能可能缺失(通过perform,或者IMP)。但是这样好像没有什么意义。

        所以我对文章中所说的组件的理解是:每个组件都在中间件注册了可以提供的服务,由需求方向中间件发起调用。那么在单个组件的测试中,需要把相应依赖的组件一起包含进来,也就是每个单独的组件工程,其实都包含了其依赖的组件。
        刘小壮:@这个App不错 是的,你理解的是对的。业务模块在单独编译运行的时候,也需要依赖用到的其他组件,比如网络、缓存这样的组件,通过中间件进行对其他组件发起调用。
        End_枫:然后对于程序中一些工具类,这些通用的工具类,需要暴露的接口非常多,比如image的扩展 十几个,UIView的扩展,十几个,NSString的等等,还有公共基类,这些东西加起来可能有几百个接口,如果把他们组件化,感觉是个很费时的工程。
        End_枫:@这个App不错 不知道我的理解对不对
      • 海浪萌物:对应组件的通信用URL的好处在哪啊?感觉URL本质上就是一个字符串,当做key来使用的
        刘小壮:@海浪萌物 是的,就像你说的那样。每个组件都有接口类,接口类中定义对外提供的服务,接口类可以调用中间件内部代码,接口类就属于中间件的一部分。
        海浪萌物:@刘小壮 @刘小壮 本质上组件化架构,是不是每个组件都有接口类,然后只有接口类对中间组件有依赖,而且接口类对组件中的其他类也有依赖关系呢?
        protocol这个还么看懂 :smile:
        刘小壮:@海浪萌物 URL本来就是当做注册表的Key的。URL的优势有很多,规范的命名、切换原生和H5页面等,这些文中都说到过。
      • 一人的灵山:关于蘑菇街的URL导航的实现,从Demo来看,当跳转到另外的VC,回退时,VC并不会被释放。这会成为问题
        刘小壮:@看看灵山 蘑菇街Demo只是为了演示,而且Demo中跳转的另一个VC,始终只会有一份内存,不会多次创建。真正项目中肯定不会这么做的。
        刘小壮:@看看灵山 先抛开Demo,MGJRouter的block实现方案,不会造成内存问题。
      • 蝴蝶之梦天使:请教下,基础组件中的 图片组件 这个怎么管理或怎么实现的?
        (1)其他组件怎么访问这个组件?
        (2)其他组件中怎么获取到图片?
        (3)在storyboard或xib中,怎么直接显示图片组件中的图片?
        刘小壮:@蝴蝶之梦天使 主工程中直接包含图片组件,所有工程引用同一份图片组件。
        蝴蝶之梦天使:怎么访问某张图片的?
        比如在主工程中,需要加载资源组件中的my.png图片,要怎么调用?
        刘小壮:@蝴蝶之梦天使 用我公司的项目解释一下。图片组件属于基础层,基础层的东西不会对上层进行引用,不会造成循环引用。所以上层模块可以直接引用图片组件,当前项目的所有工程(组件工程+主工程)引用同一个图片组件,图片组件中只存放图片资源。
      • 叮当猫喵:你好,关于网络接口的问题我想请教一下,网络接口是跟组件一起封装还是单独把app所有的网络接口统一封装起来呢?
        刘小壮:@叮当猫喵 抱歉,国庆回家过节去了,刚回来。。。
        叮当猫喵:@刘小壮 用ctmediator 模块 假如A模块为登录模块,B模块需要在组件里面点击某个按钮吊起A的登录接口,登录完了还要返回给B这个在ctmediator里面怎么实现的呢
        刘小壮:@叮当猫喵 不知道您说的是网络请求还是URL?网络是有单独模块的,通过Router进行调用。
      • 夜央未尽:想实践下,结果发现思路可以理解,只是当天项目规模太小,反而浪费实践。
        刘小壮:@夜央未尽 还是看具体项目情况吧,我们组件化做的也不是很彻底,项目也不大。
      • 6eeebdf65572:谢谢博主的分析,思路很清晰,在针对自己公司的架构实现上也能因地制宜。
        (1)博主如果能针对蘑菇街和CASA的方案还有淘宝和滴滴的多说一些自己的看法就更好了,在我看来MGJ的方案在设计上是存在缺陷的,并没有能解决一类问题。
        (2)博主提到Protocol的缺点是实现麻烦,影响开发效率。凡事皆有利弊,确实是个仁者见仁智者见智的问题,但我个人还是更倾向于Protocol,相对于Block代码结构清晰,调试维护起来相对于方便很多,这点我觉得CASA分析的很对。
        刘小壮:@MonsterSong 对于蘑菇街的两种方案和casatwy的方案,我都给出了自己的看法,在文中组件化开发总结部分。看法可能有错误,还请指出。
        刘小壮:@MonsterSong 其实我更倾向于casatwy的方案,他的方案对服务方实现了真正的解耦。其实就像你说的,每种方案各有利弊吧。
      • TianBai:微博之前的模块间通过open url进行模块之间的消息转发,但是据说后来废弃了,是为什么?
        TianBai:@刘小壮 你提醒我了,应该是URL安全性
        刘小壮:@TianBai 这就不知道了,可能是因为参数限制或安全性吧。
      • 大号鱼骨头:进阶的必经之路啊
        刘小壮:@大号鱼骨头 是要了解下 :smile:
      • captainGao:壮壮这里的文章深度可以啊 ,已变大神了啊
        刘小壮:@俊轩 我懂了
        captainGao:@刘小壮 名,李根,姓自己猜:smile::smile::smile:
        刘小壮:@俊轩 哪位朋友,报上名来 :relaxed:
      • o0下一站生活0o:一直没搞懂,我初始化的时候,这tabbar上有五个VC,这里要怎么写。这个五个VC属于不同的模块。应该是不同的组件。
        刘小壮:@嘴爷 :+1:
        嘴爷:用cocoapods拆分,我也写了一些组件开发系列的文章,欢迎参考、批评
        刘小壮:@o0下一站生活0o 初始化的是组件,控制器的展示看代码实现方式了。
      • doulala:拜读了小壮大神的文章,很详细,很受益,感谢!有一个问题希望能解答一下,我们的bundle分为了基础、中间件、功能组件,中间件与组件之间通过了router进行通信,那么基础和中间件之间是如何通讯的?还是非解耦,直接依赖?
        doulala:@刘小壮 谢谢,确实在小项目中这样会很好!您的接口和核心层具体实现是分开的么?如果是在一起,那么还是会一起编译,进而增加集成时间。另外如果就淘宝的架构来看,您认为它的基础组件和核心组件调用会是用router吗
        刘小壮:@doulala 这个还是看项目规模,和划分程度了。就像我文中说到的,我们业务层对于核心层的调用,并没有通过Router进行路由,因为项目规模小而且影响开发效率。所以我们将核心层的单个组件,都对外提供一个接口类,接口类用于消息转发,上层直接调用这个接口类。本质上也实现了一定程度的解耦,下层的改变不会对上层造成影响,哪怕我下层整体换实现方式也无所谓,对上层提供的接口类不变就可以。
        doulala:也就是您架构中的基础、核心、业务层
      • 1552e4eccec5:又抓到一只野生的大神。

        羡慕可以接触到那么多沙龙、分享~
        刘小壮:@iFuture 全靠自学,没去过沙龙和分享现场。 :blush:
      • 9ab6214fa360:还有微信的那个URL跳转“weixin://dl/profile”,最近也一直在想,但是始终想不明白,他是怎么做到页面跳转的一致性。
        就是你在“聊天内容”中点开这个URL,他会跳转到“个人信息”那个页面,然后点击“左上角”的返回,是直接回到“我”这个界面,而不是回到“聊天内容”这里,我一直希望可以实现这样的效果,但是一直想不明白要怎么实现,感觉仅仅使用路由跳转不能这样的效果吧?

        希望作者能给一些提示或者指引 :blush:
        刘小壮:@大佬杰 具体你们的需求我并不太明确,我的观点是控制好控制器之间的跳转,用好导航控制器就行,或者也可以直接addChildViewController之类的。。。
        9ab6214fa360:@刘小壮 谢谢。我之前也这样考虑过,但是貌似这适合于以tabBar为主的APP,因为目前我的APP是没有用到tabBar,所以要实现到微信的这个效果的话,会比较困难。我在考虑需不需要底层套一个tabBar并隐藏它
        刘小壮:@大佬杰 我手机可能自动更新了微信,现在上面的URL已经不能访问了。我试了一下“weixin://dl/recommendation”这个URL。猜测调用过程应该是将tab bar切换到第二个,然后用tab bar对应的导航控制器去进行push操作,之前“weixin://dl/profile”应该也是类似的道理,具体跳转逻辑就是产品需求了。所以,在微信项目中,tab bar控制器是属于主工程的,其他功能模块分别属于不同组件。
      • 9ab6214fa360:谢谢大大的文章,其实文中提到的一些文章之前也大概看过
        也了解到一些模块拆分用cocoapods管理很好,但是关于路由跳转,一直不是很了解他的用意。

        如果可以,麻烦作者可以解释一下,谢谢 :smile:
        刘小壮:@大佬杰 中间件处理的是所有组件间的通信,页面跳转只是其中一部分
        9ab6214fa360:@刘小壮 那我可不可以就理解是这个中间件(路由)的作用就是代替了以下功能?
        UIViewController *vc = [UIViewController alloc] init];
        vc.modal = self.modal;
        [self.navigationController push:vc ];
        刘小壮:@大佬杰 你说的路由跳转应该是中间件。通过模块拆分实现了项目的拆分,将项目拆分为多个组件。但是拆分为组件后,组件间总是需要通信的,所以就通过中间件进行通信,而例如MGJRouter就是中间件的一种实现方式,MGJRouter的通信就是通过URL进行调用,应该就是你说的路由。
      • b7b891f71f7b:很不错的文章
        刘小壮:@GAnYnAZ :blush:
      • godgnay:好文,我和作者一样也是看了上面那些大神的思路后一直在想将公司的项目实现模块化。在照搬模仿的过程中发现了一些问题,由于公司的项目内容并不复杂,根本不需要那么复杂的解决方案,后来产生了cocoapods管理代码的思路,目前还在完善中。主体思路就是将功能剥离,独立出一些功能形成单独的代码模块,这样代码就已经很清晰了。
        我的大名叫小爱:@刘小壮 但是还是要有这方面的思维。
        刘小壮:@shengyang_yu 是的,他们的方案适合比较大的项目,中小型项目还是根据项目情况制定架构。 :+1:
      • d14da8c11b06:壮壮越来越牛逼了
        刘小壮:@码农人生 原来大神在简书隐姓埋名,幸会幸会 :pray:
        d14da8c11b06:你雷哥
        刘小壮:@码农人生 哪位?
      • Joy___:赞
        刘小壮:@Martin_Joy :blush:

      本文标题:组件化架构漫谈

      本文链接:https://www.haomeiwen.com/subject/secwettx.html