组件化方案调研

7,753 阅读23分钟

前言

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

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

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

看上去各家都是各显神通,都有自己的技术方案,但是实际上都可以归类到如下两种方案:

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

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

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

上面这些第三方组件库的具体对比,大家可以参考霜神的这篇博客:

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

URL-Sheme方案一般都是各个组件把自己可以提供的服务通过url的形式注册到一个中心管理器,然后调用发就可以通过openURL的方式来打开这个url,然后中心管理器解析这个url,把请求转发到相应的组件去执行

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

就我个人而言,我还是比较推荐target-action方案,具体原因我们下面会进一步分析

为何要组件化

在做一件事之前我们一般都要搞清楚为什么要这么做,好处是什么,有哪些坑,这样才会有一个整体的认识,然后再决定要不要做。同样我们也要搞清楚到底需不需要实施组件化,那么就要先搞清楚什么是组件

组件的定义

组件是由一个或多个类构成,能完整描述一个业务场景,并能被其他业务场景复用的功能单位。组件就像是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

组件化方案实施

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

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