Cocoapod 组件化之中间件通信

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

一. target-action 方案 (推荐)

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

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


组件化前.png

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

组件化后.png
CTMediator实现流程如下图
组件化架构方案.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 <A_Category/CTMediator+A.h>。

然后继续编译,说找不到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 <B_Category/CTMediator+B.h>,并且把调用的代码改为:

 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 <UIKit/UIKit.h>

@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的组件化方案

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容