组件化方案调研

前言

在最前面,先祝你们除夕节快乐,开开心心过大年~~html

这篇文章主要是我近段时间针对市面上存在的一些组件化方案的调研以后,再通过本身的反思和总结写的,博客中部分文字和图借鉴自下面的博客。各位看官大爷就当作一篇读书笔记来看便可,主要是参考了以下几篇文章,另外零零散散的也看了一些其余资料,可是大多都是类似的ios

  1. 蘑菇街组件化之路
  2. iOS应用架构谈 组件化方案
  3. iOS 组件化 —— 路由设计思路分析
  4. 滴滴iOS的组件化实践与优化
  5. iOS组件化方案
  6. iOS 组件化方案探索
  7. 掌上链家组件化探索历程
  8. 京东iOS客户端组件管理实践

看上去各家都是各显神通,都有本身的技术方案,可是实际上均可以归类到以下两种方案:git

  1. 利用runtime实现的target-action方法
  2. 利用url-scheme方案

目前市面上流行的组件化方案都是经过url-scheme实现的,包括不少开源的组件化的库都是如此,只有casa的方案独树一帜,是经过Target-Action实现的github

URL-Scheme库:
  1. JLRoutes
  2. routable-ios
  3. HHRouter
  4. MGJRouter
Target-Action库:
  1. CTMediator

上面这些第三方组件库的具体对比,你们能够参考霜神的这篇博客:web

iOS 组件化 —— 路由设计思路分析json

URL-Sheme方案通常都是各个组件把本身能够提供的服务经过url的形式注册到一个中心管理器,而后调用发就能够经过openURL的方式来打开这个url,而后中心管理器解析这个url,把请求转发到相应的组件去执行设计模式

Target-Action方案利用了OC的runtime特性,无需注册,直接在原有的组件以外加一层wrapper,把对外提供的服务都抽离到该层。而后经过runtime的TARGET performSelector:ACTION withObject:PARAMS找到对应的组件,执行方法和传递参数。安全

就我我的而言,我仍是比较推荐target-action方案,具体缘由咱们下面会进一步分析bash

为什么要组件化

在作一件事以前咱们通常都要搞清楚为何要这么作,好处是什么,有哪些坑,这样才会有一个总体的认识,而后再决定要不要作。一样咱们也要搞清楚到底需不须要实施组件化,那么就要先搞清楚什么是组件架构

组件的定义

组件是由一个或多个类构成,能完整描述一个业务场景,并能被其余业务场景复用的功能单位。组件就像是PC时代我的组装电脑时购买的一个个部件,好比内存,硬盘,CPU,显示器等,拿出其中任何一个部件都能被其余的PC所使用。

因此组件能够是个广义上的概念,并不必定是页面跳转,还能够是其余不具有UI属性的服务提供者,好比日志服务,VOIP服务,内存管理服务等等。说白了咱们目标是站在更高的维度去封装功能单元。对这些功能单元进行进一步的分类,才能在具体的业务场景下作更合理的设计。

组件化的优势

纵观目前的已经在实施组件化的团队来看,你们的通常发展路径都是:前期项目小,须要快速迭代抢占市场,你们都是用传统的MVC架构去开发项目。等到后期项目愈来愈大,开发人数愈来愈多,会发现传统的开发方式致使代码管理混乱,发布、集成、测试愈来愈麻烦,被迫走向组件化的道路。

其实组件化也不是彻底必须的,若是你的团队只是开发一个小项目,团队人数小于10我的,产品线也就是两三条,那么彻底能够用传统开发方式来开发。可是若是你的团队在不断发展,产品线也愈来愈多的时候,预计后期可能会更多的时候,那么最好尽早把组件化提上议程。

摘自casa的建议:

组件化方案在App业务稳定,且规模(业务规模和开发团队规模)增加初期去实施很是重要,它助于将复杂App分而治之,也有助于多人大型团队的协同开发。但组件化方案不适合在业务不稳定的状况下过早实施,至少要等产品已经通过MVP阶段时才适合实施组件化。由于业务不稳定意味着链路不稳定,在不稳定的链路上实施组件化会致使未来主业务产生变化时,全局性模块调度和重构会变得相对复杂。

其实组件化也没有多么高大上,和咱们以前说的模块化差很少,就是把一些业务、基础功能剥离,划分为一个个的模块,而后经过pods的方式管理而已,同时要搭配一套后台的自动集成、发布、测试流程

通常当项目愈来愈大的时候,无可避免的会遇到以下的痛点:

代码冲突多,编译慢。

每一次拉下代码开发功能,开发完成准备提交代码时,每每有其余工程师提交了代码,须要从新拉去代码合并后再提交,即便开发一个很小的功能,也须要在整个工程里作编译和调试,效率较低。

迭代速度慢,耦合比较严重,没法单独测试。

各个业务模块之间互相引入,耦合严重。每次须要发版时,全部的业务线修改都须要所有回归,而后审查看是否出错,耗费大量时间。业务线之间相互依赖,可能会致使一个业务线必须等待另一个业务线开发完某个功能才能够接着开发,没法并行开发。还有一个问题,就是耦合致使没法单独测试某个业务线,可能须要等到全部业务线开发完毕,才能统一测试,浪费测试资源

为了解决上述痛点,组件化应运而生,整体来讲,组件化就是把整个项目进行拆分,分红一个个单独的可独立运行的组件,分开管理,减小依赖。 完成组件化以后,通常可达到以下效果:

  1. 加快编译速度,能够把不会常常变更的组件作成静态库,同时每一个组件能够独立编译,不依赖于主工程或者其余组件
  2. 每一个组件均可以选择本身擅长的开发模式(MVC / MVVM / MVP)
  3. 能够单独测试每一个组件
  4. 多条业务线能够并行开发,提升开发效率

如何组件化

当咱们肯定须要对项目进行组件化了,咱们第一个要解决的问题就是如何拆分组件。这是一个见仁见智的问题,没有太明确的划分边界,大体作到每一个组件只包含一个功能便可,具体实施仍是要根据实际状况权衡。

当咱们写一个类的时候,咱们会谨记高内聚,低耦合的原则去设计这个类,当涉及多个类之间交互的时候,咱们也会运用SOLID原则,或者已有的设计模式去优化设计,但在实现完整的业务模块的时候,咱们很容易忘记对这个模块去作设计上的思考,粒度越大,越难作出精细稳定的设计,我暂且把这个粒度认为是组件的粒度。

组件能够是个广义上的概念,并不必定是页面跳转,还能够是其余不具有UI属性的服务提供者,好比日志服务,VOIP服务,内存管理服务等等。说白了咱们目标是站在更高的维度去封装功能单元,把多个功能单元组合在一块儿造成一个更大的功能单元,也就是组件。对这些功能单元进行进一步的分类,才能在具体的业务场景下作更合理的设计。

下面的组件划分粒度,你们能够借鉴一下

组件化先后对比

iOS里面的组件化主要是经过cocopods把组件打包成单独的私有pod库来进行管理,这样就能够经过podfile文件,进行动态的增删和版本管理了。

下面是链家APP在实行组件化先后的对比

能够看到传统的MVC架构把全部的模块所有糅合在一块儿,是一种分布式的管理方法,耦合严重,当业务线过多的时候就会出现咱们上面说的问题。 而下图的组件化方式是一种中心Mediator的方式,让全部业务组件都分开,而后都依赖于Mediator进行统一管理,减小耦合。

组件化后,代码分类也更符合人类大脑的思考方式

组件化方案对比分析

组件化如何解决现有工程问题

传统模式的组件之间的跳转都是经过直接import,当模块比较少的时候这个方式看起来没啥问题。但到了项目愈来愈庞大,这种模式会致使每一个模块都离不开其余模块,互相依赖耦合严重。这种方式是分布式的处理方式,每一个组件都是处理和本身相关的业务。管理起来很混乱,以下图所示:

(借用霜神的几张图)

那么按照人脑的思惟方式,改为以下这种中心化的方式更加清晰明了:

可是上面这个图虽然看起来比刚开始好了许多,可是每一个组件仍是和mediator双向依赖,若是改为以下图所示就完美了:

这个时候看起来就舒服多了,每一个组件只须要本身管好本身就完了,而后由mediator负责在各个组件中间进行转发或者跳转,perfect~~ 那么如何实现这个架构呢?只要解决下面两个问题就行了:

  1. mediator做为中间件,须要经过某种方式找到每一个组件,并能调用组件的方法
  2. 每一个组件如何得知其余组件提供了哪些方法?只有这样才能够调用对方嘛

原始工程

假设咱们现有工程里面有两个组件A、B,功能很简单,以下所示。

#import <UIKit/UIKit.h>

@interface A_VC : UIViewController
-(void)action_A:(NSString*)para1;
@end

==================================

#import "A_VC.h"

@implementation A_VC

-(void)action_A:(NSString*)para1 {
    NSLog(@"call action_A %@",para1);
}

@end

复制代码
#import <UIKit/UIKit.h>

@interface B_VC : UIViewController

-(void)action_B:(NSString*)para1 para2:(NSInteger)para2;

@end

====================

#import "B_VC.h"

@implementation B_VC

-(void)action_B:(NSString*)para1 para2:(NSInteger)para2{
    NSLog(@"call action_B %@---%zd",para1,para2);
}

@end


复制代码

若是是传统作法,A、B要调用对方的功能,就会直接import对方,而后初始化,接着调用方法。如今咱们对他们实行组件化,改为如上图所示的mediator方式

target-action方案

该方案借助OC的runtime特性,实现了服务的自动发现,无需注册便可实现组件间调用。无论是从维护性、可读性、扩展性方面来说,都优于url-scheme方案,也是我比较推崇的组件化方案,下面咱们就来看看该方案如何解决上述两个问题的

Demo演示

此时A、B两个组件不用改,咱们须要加一个mediator,代码以下所示:

#import <Foundation/Foundation.h>

@interface Mediator : NSObject

-(void)A_VC_Action:(NSString*)para1;
-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2;
+ (instancetype)sharedInstance;

@end

===========================================

#import "Mediator.h"

@implementation Mediator

+ (instancetype)sharedInstance
{
    static Mediator *mediator;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        mediator = [[Mediator alloc] init];
    });
    return mediator;
}


-(void)A_VC_Action:(NSString*)para1{
    Class cls = NSClassFromString(@"A_VC");
    NSObject *target = [[cls alloc]init];
    [target performSelector:NSSelectorFromString(@"action_A:") withObject:para1];
}


-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2{
    Class cls = NSClassFromString(@"B_VC");
    NSObject *target = [[cls alloc]init];
    [target performSelector:NSSelectorFromString(@"action_B:para2:") withObject:para1 withObject:para2];
}

@end


复制代码

组件B调用组件A,以下所示:

[[Mediator sharedInstance]A_VC_Action:@"参数1"];

复制代码

组件A调用组件B,以下所示:

[[Mediator sharedInstance]B_VC_Action:@"参数1" para2:123];

复制代码

此时已经能够作到最后一张图所示的效果了,组件A,B依赖mediator,mediator不依赖组件A,B(也不是彻底不依赖,而是把用runtime特性把类的引用弱化为了字符串)

反思

看到这里,大概有人会问,既然用runtime就能够解耦取消依赖,那还要Mediator作什么?我直接在每一个组件里面用runtime调用其余组件不就完了吗,干吗还要多一个mediator?

可是这样作会存在以下问题:

  1. 调用者写起来很恶心,代码提示都没有, 参数传递很是恶心,每次调用者都要查看文档搞清楚每一个参数的key是什么,而后本身去组装成一个 NSDictionary。维护这个文档和每次都要组装参数字典很麻烦。
  2. 当调用的组件不存在的时候,无法进行统一处理

那么加一个mediator的话,就能够作到:

  1. 调用者写起来不恶心,代码提示也有了, 参数类型明确。
  2. Mediator能够作统一处理,调用某个组件方法时若是某个组件不存在,能够作相应操做,让调用者与组件间没有耦合。

改进

聪明的读者可能已经发现上面的mediator方案仍是存在一个小瑕疵,受限于performselector方法,最多只能传递两个参数,若是我想传递多个参数怎么办呢?

答案是使用字典进行传递,此时咱们还须要个组件增长一层wrapper,把对外提供的业务所有包装一次,而且接口的参数所有改为字典。 假设咱们如今的B组件须要接受多个参数,以下所示:

-(void)action_B:(NSString*)para para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
    NSLog(@"call action_B %@---%zd---%zd----%zd",para1,para2,para3,para4);
}

复制代码

那么此时须要对B组件增长一层wrapper,以下:

#import <Foundation/Foundation.h>

@interface target_B : NSObject
-(void)B_Action:(NSDictionary*)para;

@end

=================
#import "target_B.h"
#import "B_VC.h"

@implementation target_B

-(void)B_Action:(NSDictionary*)para{
    NSString *para1 = para[@"para1"];
    NSInteger para2 = [para[@"para2"]integerValue];
    NSInteger para3 = [para[@"para3"]integerValue];
    NSInteger para4 = [para[@"para4"]integerValue];
    B_VC *VC = [B_VC new];
    [VC action_B:para1 para2:para2 para3:para3 para4:para4];
}
@end

复制代码

此时mediator也须要作相应的更改,由原来直接调用组件B,改为了调用B的wrapper层:

-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
    Class cls = NSClassFromString(@"target_B");
    NSObject *target = [[cls alloc]init];
    [target performSelector:NSSelectorFromString(@"B_Action:") withObject:@{@"para1":para1, @"para2":@(para2),@"para3":@(para3),@"para4":@(para4)} ];
}

复制代码

如今的组件A调用组件B的流程以下所示:

此时的项目结构以下:

继续改进

作到这里,看似比较接近个人要求了,可是还有有点小瑕疵:

  1. Mediator 每个方法里都要写 runtime 方法,格式是肯定的,这是能够抽取出来的。
  2. 每一个组件对外方法都要在 Mediator 写一遍,组件一多 Mediator 类的长度是恐怖的。

接着优化就是casa的方案了,咱们来看看如何改进,直接看代码:

针对第一点,咱们能够抽出公共代码,当作mediator:

#import "CTMediator.h"
#import <objc/runtime.h>

@interface CTMediator ()

@property (nonatomic, strong) NSMutableDictionary *cachedTarget;

@end

@implementation CTMediator

#pragma mark - public methods
+ (instancetype)sharedInstance
{
    static CTMediator *mediator;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        mediator = [[CTMediator alloc] init];
    });
    return mediator;
}

/*
 scheme://[target]/[action]?[params]
 
 url sample:
 aaa://targetA/actionB?id=1234
 */

- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    NSString *urlString = [url query];
    for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
        NSArray *elts = [param componentsSeparatedByString:@"="];
        if([elts count] < 2) continue;
        [params setObject:[elts lastObject] forKey:[elts firstObject]];
    }
    
    // 这里这么写主要是出于安全考虑,防止黑客经过远程方式调用本地模块。这里的作法足以应对绝大多数场景,若是要求更加严苛,也能够作更加复杂的安全逻辑。
    NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
    if ([actionName hasPrefix:@"native"]) {
        return @(NO);
    }
    
    // 这个demo针对URL的路由处理很是简单,就只是取对应的target名字和method名字,但这已经足以应对绝大部份需求。若是须要拓展,能够在这个方法调用以前加入完整的路由逻辑
    id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
    if (completion) {
        if (result) {
            completion(@{@"result":result});
        } else {
            completion(nil);
        }
    }
    return result;
}

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    
    NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    Class targetClass;
    
    NSObject *target = self.cachedTarget[targetClassString];
    if (target == nil) {
        targetClass = NSClassFromString(targetClassString);
        target = [[targetClass alloc] init];
    }
    
    SEL action = NSSelectorFromString(actionString);
    
    if (target == nil) {
        // 这里是处理无响应请求的地方之一,这个demo作得比较简单,若是没有能够响应的target,就直接return了。实际开发过程当中是能够事先给一个固定的target专门用于在这个时候顶上,而后处理这种请求的
        return nil;
    }
    
    if (shouldCacheTarget) {
        self.cachedTarget[targetClassString] = target;
    }

    if ([target respondsToSelector:action]) {
        return [self safePerformAction:action target:target params:params];
    } else {
        // 有可能target是Swift对象
        actionString = [NSString stringWithFormat:@"Action_%@WithParams:", actionName];
        action = NSSelectorFromString(actionString);
        if ([target respondsToSelector:action]) {
            return [self safePerformAction:action target:target params:params];
        } else {
            // 这里是处理无响应请求的地方,若是无响应,则尝试调用对应target的notFound方法统一处理
            SEL action = NSSelectorFromString(@"notFound:");
            if ([target respondsToSelector:action]) {
                return [self safePerformAction:action target:target params:params];
            } else {
                // 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程当中,能够用前面提到的固定的target顶上的。
                [self.cachedTarget removeObjectForKey:targetClassString];
                return nil;
            }
        }
    }
}

- (void)releaseCachedTargetWithTargetName:(NSString *)targetName
{
    NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    [self.cachedTarget removeObjectForKey:targetClassString];
}

#pragma mark - private methods
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
    NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
    if(methodSig == nil) {
        return nil;
    }
    const char* retType = [methodSig methodReturnType];

    if (strcmp(retType, @encode(void)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        return nil;
    }

    if (strcmp(retType, @encode(NSInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        NSInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(BOOL)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        BOOL result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(CGFloat)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        CGFloat result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(NSUInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        NSUInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}

#pragma mark - getters and setters
- (NSMutableDictionary *)cachedTarget
{
    if (_cachedTarget == nil) {
        _cachedTarget = [[NSMutableDictionary alloc] init];
    }
    return _cachedTarget;
}

@end

复制代码

针对第二点,咱们经过把每一个组件的对外接口进行分离,剥离到多个mediator的category里面,感官上把原本在一个mediator里面实现的对外接口分离到多个category里面,方便管理

下面展现的是个组件B添加的category,组件A相似

#import "CTMediator.h"

@interface CTMediator (B_VC_Action)
-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4;

@end

====================
#import "CTMediator+B_VC_Action.h"

@implementation CTMediator (B_VC_Action)
-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
    [self performTarget:@"target_B" action:@"B_Action" params:@{@"para1":para1, @"para2":@(para2),@"para3":@(para3),@"para4":@(para4)} shouldCacheTarget:YES];
}
@end

复制代码

此时调用者只要引入该category,而后调用便可,调用逻辑其实和上面没有拆分出category是同样的。此时的项目结构以下:

URL-Scheme方案

这个方案是流传最广的,也是最多人使用的,由于Apple自己也提供了url-scheme功能,同时web端也是经过URL的方式进行路由跳转,那么很天然的iOS端就借鉴了该方案。

如何实现

Router实现代码
#import <Foundation/Foundation.h>
typedef void (^componentBlock) (NSDictionary *param);

@interface URL_Roueter : NSObject
+ (instancetype)sharedInstance;
- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk;
- (void)openURL:(NSString *)url withParam:(id)param;
@end

====================


#import "URL_Roueter.h"

@interface URL_Roueter()
@property (nonatomic, strong) NSMutableDictionary *cache;
@end


@implementation URL_Roueter

+ (instancetype)sharedInstance
{
    static URL_Roueter *router;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        router = [[URL_Roueter alloc] init];
    });
    return router;
}



-(NSMutableDictionary *)cache{
    if (!_cache) {
        _cache = [NSMutableDictionary new];
    }
    return _cache;
}


- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk {
    [self.cache setObject:blk forKey:urlPattern];
}

- (void)openURL:(NSString *)url withParam:(id)param {
    componentBlock blk = [self.cache objectForKey:url];
    if (blk) blk(param);
}


@end

复制代码
组件A
#import "A_VC.h"
#import "URL_Roueter.h"

@implementation A_VC

//把本身对外提供的服务(block)用url标记,注册到路由管理中心组件
+(void)load{
    [[URL_Roueter sharedInstance]registerURLPattern:@"test://A_Action" toHandler:^(NSDictionary* para) {
        NSString *para1 = para[@"para1"];
        [[self new] action_A:para1];
    }];
}


-(void)viewDidLoad{
    [super viewDidLoad];
    UIButton *btn = [UIButton new];
    [btn setTitle:@"调用组件B" forState:UIControlStateNormal];
    btn.frame = CGRectMake(100, 100, 100, 50);
    [btn addTarget:self action:@selector(btn_click) forControlEvents:UIControlEventTouchUpInside];
    [btn setBackgroundColor:[UIColor redColor]];
    
    self.view.backgroundColor = [UIColor blueColor];
    [self.view addSubview:btn];
    
}

//调用组件B的功能
-(void)btn_click{
    [[URL_Roueter sharedInstance]openURL:@"test://B_Action" withParam:@{@"para1":@"PARA1", @"para2":@(222),@"para3":@(333),@"para4":@(444)}];
}


-(void)action_A:(NSString*)para1 {
    NSLog(@"call action_A: %@",para1);
}

@end

复制代码

组件B实现的代码相似,就不在贴了。上面都是简化版的实现,不过核心原理是同样的。

从上面的代码能够看出来,实现原理很简单:每一个组件在本身的load方面里面,把本身对外提供的服务(回调block)经过url-scheme标记好,而后注册到URL-Router里面。

URL-Router接受各个组件的注册,用字典保存了每一个组件注册过来的url和对应的服务,只要其余组件调用了openURL方法,就会去这个字典里面根据url找到对应的block执行(也就是执行其余组件提供的服务)

存在的问题

经过url-scheme的方式去作组件化主要存在以下一些问题:

须要专门的管理后台维护

要提供一个文档专门记录每一个url和服务的对应表,每次组件改动了都要即便修改,很麻烦。参数的格式不明确,是个灵活的 dictionary,一样须要维护一份文档去查这些参数。

内存问题

每一个组件在初始化的时候都须要要路由管理中心去注册本身提供的服务,内存里须要保存一份表,组件多了会有内存问题。

混淆了本地调用和远程调用

url-scheme是Apple拿来作app之间跳转的,或者经过url方式打开APP,可是上述的方案去把他拿来作本地组件间的跳转,这会产生问题,大概分为两点:

  1. 远程调用和本地调用的处理逻辑是不一样的,正确的作法应该是把远程调用经过一个中间层转化为本地调用,若是把二者二者混为一谈,后期可能会出现没法区分业务的状况。好比对于组件没法响应的问题,远程调用可能直接显示一个404页面,可是本地调用可能须要作其余处理。若是不加以区分,那么久没法完成这种业务要求。

  2. 远程调用只能传能被序列化为json的数据,像 UIImage这样很是规的对象是不行的。因此若是组件接口要考虑远程调用,这里的参数就不能是这类很是规对象,接口的定义就受限了。出现这种状况的缘由就是,远程调用是本地调用的子集,这里混在一块儿致使组件只能提供子集功能(远程调用),因此这个方案是天生有缺陷的

  3. 理论上来说,组件化是接口层面的东西,应该用语言自身的特性去解决,而url是用于远程通讯的,不该该和组件化扯上关系

改进

针对上述第二点描述的没法传递常规对象的问题,蘑菇街作了改进,经过protocol转class的方式去实现,可是我想说这种实现办法真是越高越复杂了。具体看代码就知道了

protocolMediator实现:
功能:经过protocol的字符串存储class

#import <Foundation/Foundation.h>

@interface ProtocolMediator : NSObject
+ (instancetype)sharedInstance;
- (void)registerProtocol:(Protocol *)proto forClass:(Class)cls;
- (Class)classForProtocol:(Protocol *)proto;

@end

============

#import "ProtocolMediator.h"

@interface ProtocolMediator()
@property (nonatomic,strong) NSMutableDictionary *protocolCache;

@end
@implementation ProtocolMediator


+ (instancetype)sharedInstance
{
static ProtocolMediator *mediator;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    mediator = [[ProtocolMediator alloc] init];
});
return mediator;
}

-(NSMutableDictionary *)protocolCache{
    if (!_protocolCache) {
        _protocolCache = [NSMutableDictionary new];
    }
    return _protocolCache;
}

- (void)registerProtocol:(Protocol *)proto forClass:(Class)cls {
    [self.protocolCache setObject:cls forKey:NSStringFromProtocol(proto)];
}

- (Class)classForProtocol:(Protocol *)proto {
    return self.protocolCache[NSStringFromProtocol(proto)];
}


@end

复制代码
commonProtocol实现:
功能:全部须要传递很是规参数的方法都放在这里定义,而后各个组件本身去具体实现(这里为了演示方便,使用的常规的字符串和int类型。固然也能够传递UIImage等很是规对象)

#import <Foundation/Foundation.h>

@protocol A_VC_Protocol <NSObject>
-(void)action_A:(NSString*)para1;

@end

@protocol B_VC_Protocol <NSObject>
-(void)action_B:(NSString*)para para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4;
@end

复制代码
组件A实现:
#import <UIKit/UIKit.h>
#import "CommonProtocol.h"

@interface A_VC : UIViewController<A_VC_Protocol>
@end


=============================

#import "A_VC.h"
#import "ProtocolMediator.h"


@implementation A_VC

//注册本身的class
+(void)load{
    [[ProtocolMediator sharedInstance] registerProtocol:@protocol(A_VC_Protocol) forClass:[self class]];

}
     

//调用组件B,先经过protocol字符串取出类class,而后再实例化之调用组件B的方法    
-(void)btn_click{
    Class cls = [[ProtocolMediator sharedInstance] classForProtocol:@protocol(B_VC_Protocol)];
    UIViewController<B_VC_Protocol> *B_VC = [[cls alloc] init];
    [B_VC action_B:@"param1" para2:222 para3:333 para4:444];
}


-(void)action_A:(NSString*)para1 {
    NSLog(@"call action_A: %@",para1);
}

@end

复制代码
组件B实现
#import <UIKit/UIKit.h>
#import "CommonProtocol.h"


@interface B_VC : UIViewController<B_VC_Protocol>
@end

=============

#import "B_VC.h"
#import "ProtocolMediator.h"

@implementation B_VC

+(void)load{
    [[ProtocolMediator sharedInstance] registerProtocol:@protocol(B_VC_Protocol) forClass:[self class]];
}


-(void)btn_click{
    Class cls = [[ProtocolMediator sharedInstance] classForProtocol:@protocol(A_VC_Protocol)];
    UIViewController<A_VC_Protocol> *A_VC = [[cls alloc] init];
    [A_VC action_A:@"param1"];
}


-(void)action_B:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
    NSLog(@"call action_B: %@---%zd---%zd---%zd",para1,para2,para3,para4);
}

@end


复制代码

原理和缺点

每一个组件先经过 Mediator 拿到其余的组件对象class,而后在实例化该class为实例对象,再经过该对象去调用它自身实现的protocol方法,由于是经过接口的形式实现的方法,因此任何类型参数都是能够传递的。

可是这会致使一个问题:组件方法的调用是分散在各地的,没有统一的入口,也就无法作组件不存在时的统一处理。

从上面的实现就能够看出来A调用B不是直接经过mediator去调用,而是先经过mediator生成其余组件的对象,而后本身再用该对象去调用其余组件的方法,这就致使组件方法调用分散在各个调用组件内部,而不能像target-action方案那样对全部组件的方法调用进行统一的管理。

再者这种方式让组件同时依赖两个中心:ProtocolMediator和CommonProtocol,依赖越多,后期扩展和迁移也会相对困难。

而且这种调用其余组件的方式有点诡异,不是正常的使用方法,通常都是直接你发起一个调用请求,其余组件直接把执行结果告诉你,可是这里确实给你返回一个组件对象,让你本身在用这个对象去发起请求,这操做有点蛋疼。。。

总结

其实蘑菇街的url-scheme加上protocol-class方案一块儿提供组件间跳转和调用会让人无所适从,使用者还要区分不一样的参数要使用的不一样的方法,而target-action方案能够用相同的方法来传递任意参数。综上所述,target-action方案更优。

Demo下载

  1. url-scheme
  2. protocol-class
  3. target-action

组件化方案实施

从早上起床写到凌晨,实在写不动了,留个坑,过年来在写。

收拾收拾行李准备回家过年啦,提早给你们拜个早年 ~~~

相关文章
相关标签/搜索