Cocoapod 组件化之中间件通信

app内部的路由设计主要两种思路: target-action 和 url-scheme 方案

一. target-action 方案 (推荐)

CTMediator
该方案借助OC的runtime特性,通过实现了服务的自动发现,无需注册即可实现组件间调用。优于url-scheme方案,所以重点讲一下CTMediator及其使用。

组件化之前各组件页面通过import调用,互相侵入,耦合严重


Cocoapod 组件化之中间件通信_第1张图片
组件化前.png

组件化之后,把组件具体的方法都写在Category里,调用的方式都是performTarget: action: params: shouldCacheTarget:方法,最终去掉了中间件Mediator对组件的依赖,各个组件之间互相不再依赖,组件间调用只依赖中间者Mediator,Mediator不依赖其他任何组件。

Cocoapod 组件化之中间件通信_第2张图片
组件化后.png
CTMediator实现流程如下图
Cocoapod 组件化之中间件通信_第3张图片
组件化架构方案.png
在已有工程中实施该方案分为四步
1 创建私有Pod工程和Category工程

主工程MainProject假设为一个非常简单的应用,一共就三个页面。首页push了AViewController,AViewController里又push了BViewController。我们可以理解成这个工程由三个业务组成:首页、A业务、B业务。

创建私有仓库 A 和A_category

我们这一次组件化的实施目标就是把A业务组件化出来,首页和B业务都还放在主工程。

2 在主工程中引入A_Category工程,并让主工程编译通过

通过cocoapods 引入A_Category,
去主工程的Podfile下添加pod "A_Category", :path => "../A_Category"来本地引用A_Category。

然后编译一下,说找不到AViewController的头文件。此时我们把头文件引用改成#import

然后继续编译,说找不到AViewController这个类型。看一下这里是使用了AViewController的地方,于是我们在Development Pods下找到CTMediator+A.h,在里面添加一个方法:

 - (UIViewController *)A_aViewController;

再去CTMediator+A.m中,补上这个方法的实现,把主工程中调用的语句作为注释放进去,将来写Target-Action要用:

- (UIViewController *)A_aViewController
{
    /*
        AViewController *viewController = [[AViewController alloc] init];
     */
    return [self performTarget:@"A" action:@"viewController" params:nil shouldCacheTarget:NO];
}

补充说明一下,performTarget:@"A"中给到的@"A"其实是Target对象的名字。一般来说,一个业务Pod只需要有一个Target就够了,但一个Target下可以有很多个Action。Action的名字也是可以随意命名的,只要到时候Target对象中能够给到对应的Action就可以了。

关于Target-Action我们会在第三步中去实现,现在不实现Target-Action是不影响主工程编译的。

category里面这么写就已经结束了,后面的实施过程中就不会再改动到它了。

然后我们把主工程调用AViewController的地方改为基于CTMediator Category的实现:

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

再编译一下,编译通过。

到此为止主工程就改完了,现在跑主工程点击这个按钮跳不到A页面是正常的,因为我们还没有在A工程中实现Target-Action。

而且此时主工程中关于A业务的改动就全部结束了,后面的组件化实施过程中,就不会再有针对A业务线对主工程的改动了。

3 添加Target-Action,并让A工程编译通过

关掉所有XCode窗口。然后打开两个工程:A_Category工程和A工程。

我们在A工程中创建一个文件夹:Targets,然后看到A_Category里面有performTarget:@"A",所以我们新建一个对象,叫做Target_A。

然后又看到对应的Action是viewController,于是在Target_A中新建一个方法:Action_viewController。Target对象如下:

@interface Target_A : NSObject
- (UIViewController *)Action_viewController:(NSDictionary *)params;

@end

实现文件:
#import "Target_A.h"
#import "AViewController.h"

@implementation Target_A

- (UIViewController *)Action_viewController:(NSDictionary *)params
{
    AViewController *viewController = [[AViewController alloc] init];
    return viewController;
}

@end

因为Target对象处于A的命名域中,所以Target对象中可以随意import A业务线中的任何头文件。

Target对象的Action不仅仅用于返回ViewController实例的,它可以用来执行各种属于业务线本身的任务。例如上传文件,转码等等各种任务其实都可以作为一个Action来给外部调用,Action完成这些任务的时候,业务逻辑是可以写在Action方法里面的。

Action具备调度业务线提供的任何对象和方法来完成自己的任务的能力。它的本质就是对外业务的一层服务化封装。

然后我们再继续编译A工程,发现找不到BViewController。由于我们这次组件化实施的目的仅仅是将A业务线抽出来,BViewController是属于B业务线的,所以我们没必要把B业务也从主工程里面抽出来。但为了能够让A工程编译通过,我们需要提供一个B_Category来使得A工程可以调度到B,同时也能够编译通过。

B_Category的创建步骤跟A_Category是一样的:新建Xcode工程、新建Repo、配置Repo、添加Category代码。

B_Category添加好后,我们同样在A工程的Podfile中本地指过去,然后跟在主工程的时候一样。

@interface CTMediator (B)

- (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText;

@end

实现文件:
#import "CTMediator+B.h"

@implementation CTMediator (B)

- (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText
{
    /*
        BViewController *viewController = [[BViewController alloc] initWithContentText:@"hello, world!"];
     */
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    params[@"contentText"] = contentText;
    return [self performTarget:@"B" action:@"viewController" params:params shouldCacheTarget:NO];
}

@end

然后我们对应地在A工程中修改头文件引用为#import ,并且把调用的代码改为:

 UIViewController *viewController = [[CTMediator sharedInstance] B_viewControllerWithContentText:@"hello, world!"];
    [self.navigationController pushViewController:viewController animated:YES];

此时再编译一下,编译通过了。注意哦,这里A业务线跟B业务线就已经完全解耦了,跟主工程就也已经完全解耦了。

4 收尾

此时我们给B业务线创建了Category,但没有创建Target-Action。所以我们要去主工程创建一个B业务线的Target-Action。创建的时候其实完全不需要动到B业务线的代码,只需要新增Target_B对象即可:

Target_B头文件:
#import 

@interface Target_B : NSObject

- (UIViewController *)Action_viewController:(NSDictionary *)params;

@end

Target_B实现文件:
#import "Target_B.h"
#import "BViewController.h"

@implementation Target_B

- (UIViewController *)Action_viewController:(NSDictionary *)params
{
    NSString *contentText = params[@"contentText"];
    BViewController *viewController = [[BViewController alloc] initWithContentText:contentText];
    return viewController;
}

@end

这个Target对象在主工程内不存在任何侵入性,将来如果B要独立成一个组件的话,把这个Target对象带上就可以了。

收尾工作就到此结束,我们创建了三个私有Pod:A、A_Category、B_Category。

接下来我们要做的事情就是给这三个私有Pod发版,所有的Pod发完版之后,我们再把Podfile里原来的本地引用改回正常引用

二. url-scheme 方案

MGJRouter

#import 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
#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执行(也就是执行其他组件提供的服务)

组件化架构漫谈
iOS组件化
iOS组件化 - 路由设计思路分析
在现有工程中实施基于CTMediator的组件化方案

你可能感兴趣的:(Cocoapod 组件化之中间件通信)