iOS 组件化方案

    xiaoxiao2021-03-25  250

    casatwy组件化方案

    整体架构

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

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

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

    casatwy提出的组件化架构

    架构设计思路

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

    但如果CTMediator类对外提供的方法都放在此类中,将会对CTMediator造成极大的负担和代码量。解决方法就是对每个服务方组件创建一个CTMediator的Category,并将对服务方的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中定义的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的方式调用,这个过程就叫做发现服务

    我们发现,在这个方法中其实做了参数处理和内部调用的功能,这样就可以保证组件内部的业务不受外部影响,对内部业务没有侵入性

    - (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,被调用的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之类的操作的。

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

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

    不推荐的集成方式

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


    组件化开发总结

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

    架构对比

    在除蘑菇街Protocol方案外,其他两种方案都或多或少的存在硬编码问题,硬编码如果量比较大的话挺麻烦的。 在casatwy的CTMediator方案中需要硬编码Target、Action字符串,只不过这个缺陷被封闭在中间件里面了,将这些字符串都统一定义为常量,外界使用不需要接触到硬编码。蘑菇街的MGJRouter的方案也是一样的,也有硬编码URL的问题,蘑菇街可能也做了类似的处理。

    casatwy和蘑菇街提出的两套组件化方案,大体结构是类似的,三套方案都分为调用方中间件服务方,只是在具体实现过程中有些不同。例如Protocol方案在中间件中加入了Protocol文件,casatwy的方案在中间件中加入了Category。 三种方案内部都有容错处理,所以三种方案的稳定性都是比较好的,而且都可以拿出来单独运行,在服务方不存在的情况下也不会有问题。

    在三套方案中,服务方都对外提供一个供外界调用的接口类,这个类中实现组件对外提供的服务,中间件通过接口类来实现组件间的通信。在此类中统一定义对外提供的服务,外界调用时就知道服务方可以做什么。

    调用流程也不大一样,蘑菇街的两套方案都需要注册操作,无论是Block还是Protocol都需要注册后才可以提供服务。而casatwy的方案则不需要,直接通过runtime调用。casatwy的方案实现了真正的对服务方解耦,而蘑菇街的两套方案则没有,对服务方和调用方都造成了耦合。

    我认为三套方案中,Protocol方案是调用和维护最麻烦的一套方案。维护时需要同时维护Protocol、接口类两部分。而且调用时需要将服务方的接口类返回给调用方,并由调用方执行一系列调用逻辑,调用一个服务的逻辑非常复杂,这在开发中是非常影响开发效率的。 

    总结

    下面是组件化开发中的一个小总结,也是开发过程中的一些注意点。
    在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方案弥补这个问题。

    组件化开发可以很好的提升代码复用性,组件可以直接拿到其他项目中使用,这个优点在下面淘宝架构中会着重讲一下。

    对于调试工作,应该放在每个组件中完成。单独的业务组件可以直接提交给测试提测,这样测试起来也比较方便。最后组件开发完成并测试通过后,再将所有组件更新到主项目,提交给测试进行集成测试即可。

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

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

    我公司架构

    下面就简单说说我公司项目架构,公司项目是一个地图导航应用,业务层之下的基础组件占比较大。且基础组件相对比较独立,对外提供了很多调用接口。刚开始想的是采用MGJRouter的方案,但如果这些调用都通过Router进行,开发起来比较复杂,反而会适得其反。最主要我们项目也并不是非常大,没必要都用Router转发。

    对于这个问题,公司项目的架构设计是:层级架构+组件化架构,组件化架构处于层级架构的最上层,也就是业务层。采取这种结构混合的方式进行整体架构,这个对于公共组件的管理和层级划分比较有利,符合公司业务需求。

    公司组件化架构

    对于业务层级依然采用组件化架构的设计,这样可以充分利用组件化架构的优势,对项目组件间进行解耦。在上层和下层的调用中,下层的功能组件应该对外开放一个接口类,在接口类中声明所有的服务,实现上层调用当前组件的一个中转,上层直接调用接口类。这样做的好处在于,如果下层发生改变不会对上层造成影响,而且也省去了部分Router转发的工作。

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

    为什么这么做?

    首先就像我刚才说的,我公司项目并不是很大,根本没必要拆分的那么彻底。

    因为组件化开发有一个很重要的原因就是解耦合,如果我做到了底层不对上层依赖,这样就已经解除了上下层的相互耦合。而且上层对下层进行调用的时候,也不是直接调用下层,通过一个接口类进行中转,实现了下层的改变对上层无影响,这也是上层对下层解耦的表现。

    所以对于第三方就不用说了,上层直接调用下层的第三方也是没问题的,这都是解耦的。

    模型类怎么办,放在哪合适?

    casatwy对模型类的观点是去Model化,简单来说就是用字典代替Model存储数据。这对于组件化架构来说,是解决组件之间数据传递的一个很好的方法。

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

    那么如果把模型类放在Router中,这样会造成Router耦合了业务,造成业务的侵入性。如果在用到这个模型对象的所有组件中,都分别维护一份相同的模型类,这样之后业务发生改变模型类就会很麻烦。

    那应该怎么办呢?

    如果将模型类单独拉出来,定义一个模型组件呢?这个看起来比较可行,将这个定义模型的组件下沉到下层,模型组件不包含业务,只声明模型对象的类。但是一般组件的模型对象都是当前组件内使用的,将模型对象传递给其他组件的需求非常少,那所有的模型类都定义到模型组件吗

    对于这个问题,我建议在项目开发中将模型类还定义在当前业务组件中,在组件间传递模型对象时进行去Model化,传递字典类型的参数。 上面只是思考,恰巧我公司持久化方案用的是CoreData,所有模型的定义都在CoreData组件中,这样就避免了业务层组件之间因为模型类的耦合。

    滴滴组件化架构

    之前看过滴滴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

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

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

    Bundle即App容器即OS,所有Bundle App被集成到OS上,使每个组件的开发就像app开发一样简单。这样就做到了从巨型app回归普通app的轻盈,使大型项目的开发问题彻底得到了解决。

    转载请注明原文地址: https://ju.6miu.com/read-267.html

    最新回复(0)