美文网首页
组件化一些见解

组件化一些见解

作者: Dolphii | 来源:发表于2019-08-23 18:05 被阅读0次

前言

这篇文章主要是对MGJRouter和CTMediator组件框架调研之后写的介绍与理解。
主要也是市面比较主流是URL-Scheme和Target-Action两种方式,下面我会对这两种分别说明并且附上demo。

组件化的优点

  • 业务划分更加清晰,新人接手更加容易,可以按组件分配开发任务。
  • 项目可维护性更强,提高开发效率
  • 单独测试某个组件
  • 加快编译速度,不需要编译整个项目代码

URL-Scheme库(蘑菇街MGJRouter)

URL-Scheme库我以蘑菇街为例,其他的库也大同小异,有兴趣可以去看。

  • JLRoutes
  • routable-ios
  • HHRouter
  • MGJRouter
    蘑菇街通过MGJRouter中间层,通过MGJRouter进行组件间的消息转发,更像一种路由的形式。实现方式大致是,在提供服务的组件中提前注册block,然后调用方组件通过URL调用block。

蘑菇街实现方式

注册代码

@interface MGJRouter ()
/**
 *  保存了所有已注册的 URL
 *  结构类似 @{@"beauty": @{@":id": {@"_", [block copy]}}}
 */
@property (nonatomic) NSMutableDictionary *routes;
@end

@implementation MGJRouter

+ (instancetype)sharedInstance
{
    static MGJRouter *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

+ (void)registerURLPattern:(NSString *)URLPattern toHandler:(MGJRouterHandler)handler
{
    [[self sharedInstance] addURLPattern:URLPattern andHandler:handler];
}
    __weak typeof(self)weak = self;
    [MGJRouter registerURLPattern:@"zhs://businessa/changeText" toHandler:^(NSDictionary *routerParameters) {
        NSDictionary *userInfo = routerParameters[MGJRouterParameterUserInfo];
        NSLog(@"----------------%@",userInfo);
        weak.updateText = userInfo[@"changeText"];
        weak.updateLabel.text = weak.updateText;
    }];
[MGJRouter openURL:@"zhs://businessa/changeText" withUserInfo:@{@"changeText":@"组件B改变了我"} completion:^(id result) {
        NSLog(@"---------组件B改变了A的文字---------");
    }];

每个组件都需要提前注册,通过URL保存需要执行的Block代码到Router里面,URL-Router接受各个组件的注册,用字典保存了每个组件注册过来的URL和对应的服务,只要其他组件调用了openURL方法,就会去这个字典里面根据URL找到对应的block执行(也就是执行其他组件提供的服务)
也可以通过objectForURL执行block之后得到返回值。

+ (void)load {
    [MGJRouter registerURLPattern:@"zhs://businessa/createa" toObjectHandler:^id(NSDictionary *routerParameters) {
        NSDictionary *userInfo = routerParameters[MGJRouterParameterUserInfo];
        RouterBusinessAVC *businessVC = [[RouterBusinessAVC alloc]initWithId:userInfo[@"uuid"] text:userInfo[@"text"]];
        return businessVC;
    }];
}
id businessVC = [MGJRouter objectForURL:@"zhs://businessa/createa" withUserInfo:@{@"uuid":@"uw93428",@"text":@"我是通过MGJRouter过来的"}];
[self.navigationController pushViewController:businessVC animated:YES];

可以看出这种方式,里面有很多硬编码,某个URL不小心写错就会匹配不到,另外参数都是字典,数据类型不明确,修改参数比较危险,需要组件去对应修改,并不会编译报错。对于这个问题,蘑菇街在此基础上推出Protocol-Class方案。

Protocol-Class方案

Protocol-Class方案和上面路由差别不是很大,只是把URL->Block变为Protocol-Class的形式。
每个组件都有一个protocol,组件实现了协议里面的方法,供其他组件调用,相当于通过Protocol,组件对外提供一个可被调用的方法列表。
有一个ComponentProtocol用来引入组件协议,方便各组组件互相调用。
和MGJRouter一样,每个组件都要注册,程序开始运行时将自身的Class注册到Manager中,用Protocol反射出字符串当做key。
代码实例:

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

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

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

注册:

+ (void)load {
    [[ModuleManager sharedInstance]registerClass:[self class] forProtocol:@protocol(ProtocolA)];
}

组件A协议的方法

@protocol ProtocolA <NSObject>

- (void)configureAVCWithUuid:(NSString *)uuid
                        text:(NSString *)text;

@end

- (void)configureAVCWithUuid:(NSString *)uuid
                        text:(NSString *)text {
    self.updateText = text;
    self.uuid = uuid;
}

使用:

Class vcClass = [[ModuleManager sharedInstance]classForProtocol:@protocol(ProtocolA)];
UIViewController<ProtocolA> *compontAVC = [[vcClass alloc]init];
[compontAVC configureAVCWithUuid:@"i9342f8" text:@"我是通过Protocol过来的"];
[self.navigationController pushViewController:compontAVC animated:YES];

这种方式弥补了上面URL-Router硬编码和参数不明确的问题,但是组件方法的调用是分散在各地的,没有统一的入口,也就没法做组件方法不存在时的统一处理,使用的这个方法的每个组件都需要去处理。蘑菇街是两种方式混用,使用者还要区分不同的参数要使用的不同的方法,这种其实也不太友好。

内存问题

蘑菇街这两种方式都有注册到字典,常驻内存。

  • block实现方式可能导致的内存问题,需要避免循环引用的问题。
    经过暴力测试,证明并不会导致内存问题。被保存在字典中是一个block对象,而block对象本身并不会占用多少内存。在调用block后会对block体中的方法进行执行,执行完成后block体中的对象释放。
    block自身的实现只是一个结构体,也就相当于字典中存放的是很多结构体,所以内存的占用并不是很大。
  • 对于协议这种实现方式,和block内存常驻方式差不多。只是将存储的block对象换成Class对象,如果不是已经实例化的对象,内存占用还是比较小的。

Target-Action

这个是casatwy大神提出来的,利用runtime特性,不需要注册,在外层通过CTMediator调用performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget找到对应的组件,执行方法和传递参数。CTMediator内部主要是通过[target performSelector:action withObject:params]去执行对应组件的方法,接下来我们分开来讲target、action、params。

Target

[target performSelector:action withObject:params]中这个target是方法执行的类,每一个组件都有一个执行的Target,里面放着这个组件对外可调用方法。CTMediator这个Target命名也有规定,Target_前缀开始,避免命名冲突。

#import "Target_CompoentA.h"
#import "CTCompoentAVC.h"

@implementation Target_CompoentA

- (UIViewController *)Action_compoentAVC:(NSDictionary *)params{
    CTCompoentAVC *vc = [[CTCompoentAVC alloc]initWithUuid:params[@"uuid"] text:params[@"text"]];
    return vc;
}

@end

Action

[target performSelector:action withObject:params]中这个action是被执行的方法,定义在target里面,action有Action_作为前缀,每个组件命名最好根据组件来,这样好区分。

#import "Target_CompoentB.h"
#import "CTCompoentBVC.h"

@implementation Target_CompoentB

- (UIViewController *)Action_compoentBVC:(NSDictionary *)params{
    CTCompoentBVC *vc = [[CTCompoentBVC alloc]init];
    return vc;
}

- (UIViewController *)Action_compoentBVCwithBlock:(NSDictionary *)params {
    CTCompoentBVC *vc = [[CTCompoentBVC alloc]initWithBlockParams:params];
    return vc;
}

@end

这样组件定义好了方法就可以通过CTMediator去调用了,如果组件随着业务变多,方法更多,那这个类里面方法会越来越多,类越来越大,用category可以解决这个问题,各个组件方法都分散到各自组件的category里面去。

#import "CTMediator+CompoentAAction.h"

NSString *const kCTMediatorTargetA = @"CompoentA";

NSString *const kCTMediatorTargetARootVC = @"compoentAVC";

@implementation CTMediator (CompoentAAction)

- (UIViewController *)compoentAVC {
    UIViewController *vc = [self performTarget:kCTMediatorTargetA action:kCTMediatorTargetARootVC params:@{@"uuid":@"ei3423d",@"text":@"我是从Mediator过来的"} shouldCacheTarget:NO];
    if([vc isKindOfClass:[UIViewController class]]){
        return vc;
    }
    return nil;
}

@end
@implementation CTMediator (CompoentBAction)

- (UIViewController *)compoentBVC {
    UIViewController *vc = [self performTarget:kCTMediatorTargetB action:kCTMediatorTargetBRootVC params:@{@"uuid":@"9oieru3"} shouldCacheTarget:NO];
    if([vc isKindOfClass:[UIViewController class]]){
        return vc;
    }
    return nil;
}

@end

调用组件方法

UIViewController *compoentAVC = [[CTMediator sharedInstance] compoentAVC];
[self.navigationController pushViewController:compoentAVC animated:YES];

CTMediator不用注册,可以直接调用。如果某个组件的方法参数变了,修改组件及组件对应CTMediator的categroy方法,如果里面带参数的话,需要修改其他组件,不修改会编译报错预警,MGJRouter需要到组件去修改,可以编译通过。CTMediator命名需要按照规则,命名大写字母开头加_,可能对一些开发者有点不适应。

总结

蘑菇街方式都需要维护一张表,参数类型和修改参数对开发者也不是很友好,硬编码比较多;CTMediator不用注册,但是创建的类比较多,不是很直观,另外组件间的交互需要额外再设计。

摘自casa的建议:

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

tips

前期开发中项目可能项目比较小,可能组件化可能更复杂,但是后期产品线多,业务更多,那组件化就需要提上议程,开发时就需要做好这个业务模块抽离出来打成私有pod准备。需要注意以下几点:

  • 基础功能打成私有pod ,然后尽量用里面的方法
  • 图片资源尽量放在一起,公有图片资源抽出
  • token最好传值进去,因为有可能这个业务模块被好几个app使用,每个app获取token的方式都不一样
  • 业务模块的统计最好也抽离出来

参考

组件化架构漫谈
组件化方案调研
组件化方案

demo

demo地址

相关文章

网友评论

      本文标题:组件化一些见解

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