iOS 组件化方案探索

2016-3-18

看了 Limboy(文章1 文章2) 和 Casa (文章) 对 iOS 组件化方案的讨论,写篇文章梳理下思路。

首先我觉得”组件”在这里不太合适,因为按我理解组件是指比较小的功能块,这些组件不需要多少组件间通信,没什么依赖,也就不需要做什么其他处理,面向对象就能搞定。而这里提到的是较大粒度的业务功能,我们习惯称为”模块”。为了方便表述,下面模块和组件代表同一个意思,都是指较大粒度的业务模块。

一个 APP 有多个模块,模块之间会通信,互相调用,例如微信读书有 书籍详情 想法列表 阅读器 发现卡片 等等模块,这些模块会互相调用,例如 书籍详情要调起阅读器和想法列表,阅读器要调起想法列表和书籍详情,等等,一般我们是怎样调用呢,以阅读器为例,会这样写:

#import "WRBookDetailViewController.h"
#import "WRReviewViewController.h"
@implementation WRReadingViewController
- (void)gotoDetail {
 WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:self.bookId];
 [self.navigationController pushViewController:detailVC animated:YES];
}

- (void)gotoReview {
 WRReviewViewController *reviewVC = [[WRReviewViewController alloc] initWithBookId:self.bookId reviewType:1];
 [self.navigationController pushViewController:reviewVC animated:YES];
}
@end

看起来挺好,这样做简单明了,没有多余的东西,项目初期推荐这样快速开发,但到了项目越来越庞大,这种方式会有什么问题呢?显而易见,每个模块都离不开其他模块,互相依赖粘在一起成为一坨:

component1

这样揉成一坨对测试/编译/开发效率/后续扩展都有一些坏处,那怎么解开这一坨呢。很简单,按软件工程的思路,下意识就会加一个中间层:

component2

叫他 Mediator Manager Router 什么都行,反正就是负责转发信息的中间层,暂且叫他 Mediator。

看起来顺眼多了,但这里有几个问题:

  1. Mediator 怎么去转发组件间调用?
  2. 一个模块只跟 Mediator 通信,怎么知道另一个模块提供了什么接口?
  3. 按上图的画法,模块和 Mediator 间互相依赖,怎样破除这个依赖?

方案1

对于前两个问题,最直接的反应就是在 Mediator 直接提供接口,调用对应模块的方法:

//Mediator.m
#import "BookDetailComponent.h"
#import "ReviewComponent.h"
@implementation Mediator
+ (UIViewController *)BookDetailComponent_viewController:(NSString *)bookId {
 return [BookDetailComponent detailViewController:bookId];
}
+ (UIViewController *)ReviewComponent_viewController:(NSString *)bookId reviewType:(NSInteger)type {
 return [ReviewComponent reviewViewController:bookId type:type];
}
@end
//BookDetailComponent 组件
#import "Mediator.h"
#import "WRBookDetailViewController.h"
@implementation BookDetailComponent
+ (UIViewController *)detailViewController:(NSString *)bookId {
 WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:bookId];
 return detailVC;
}
@end
//ReviewComponent 组件
#import "Mediator.h"
#import "WRReviewViewController.h"
@implementation ReviewComponent
+ (UIViewController *)reviewViewController:(NSString *)bookId type:(NSInteger)type {
 UIViewController *reviewVC = [[WRReviewViewController alloc] initWithBookId:bookId type:type];
 return reviewVC;
}
@end

然后在阅读模块里:

//WRReadingViewController.m
#import "Mediator.h"
@implementation WRReadingViewController
- (void)gotoDetail:(NSString *)bookId {
 UIViewController *detailVC = [Mediator BookDetailComponent_viewControllerForDetail:bookId];
 [self.navigationController pushViewController:detailVC];

 UIViewController *reviewVC = [Mediator ReviewComponent_viewController:bookId type:1];
 [self.navigationController pushViewController:reviewVC];
}
@end

这就是一开始架构图的实现,看起来显然这样做并没有什么好处,依赖关系并没有解除,Mediator 依赖了所有模块,而调用者又依赖 Mediator,最后还是一坨互相依赖,跟原来没有 Mediator 的方案相比除了更麻烦点其他没区别。

那怎么办呢。

怎样让Mediator解除对各个组件的依赖,同时又能调到各个组件暴露出来的方法?对于OC有一个法宝可以做到,就是runtime反射调用:

//Mediator.m
@implementation Mediator
+ (UIViewController *)BookDetailComponent_viewController:(NSString *)bookId {
 Class cls = NSClassFromString(@"BookDetailComponent");
 return [cls performSelector:NSSelectorFromString(@"detailViewController:") withObject:@{@"bookId":bookId}];
}
+ (UIViewController *)ReviewComponent_viewController:(NSString *)bookId type:(NSInteger)type {
 Class cls = NSClassFromString(@"ReviewComponent");
 return [cls performSelector:NSSelectorFromString(@"reviewViewController:") withObject:@{@"bookId":bookId, @"type": @(type)}];
}
@end

这下 Mediator 没有再对各个组件有依赖了,你看已经不需要 #import 什么东西了,对应的架构图就变成:

component3

只有调用其他组件接口时才需要依赖 Mediator,组件开发者不需要知道 Mediator 的存在。

等等,既然用runtime就可以解耦取消依赖,那还要Mediator做什么?组件间调用时直接用runtime接口调不就行了,这样就可以没有任何依赖就完成调用:

//WRReadingViewController.m
@implementation WRReadingViewController
- (void)gotoReview:(NSString *)bookId {
 Class cls = NSClassFromString(@"ReviewComponent");
 UIViewController *reviewVC = [cls performSelector:NSSelectorFromString(@"reviewViewController:") withObject:@{@"bookId":bookId, @"type": @(1)}];
 [self.navigationController pushViewController:reviewVC];
}
@end

这样就完全解耦了,但这样做的问题是:

  1. 调用者写起来很恶心,代码提示都没有,每次调用写一坨。
  2. runtime方法的参数个数和类型限制,导致只能每个接口都统一传一个 NSDictionary。这个 NSDictionary里的key value是什么不明确,需要找个地方写文档说明和查看。
  3. 编译器层面不依赖其他组件,实际上还是依赖了,直接在这里调用,没有引入调用的组件时就挂了

把它移到Mediator后:

  1. 调用者写起来不恶心,代码提示也有了。
  2. 参数类型和个数无限制,由 Mediator 去转就行了,组件提供的还是一个 NSDictionary 参数的接口,但在Mediator 里可以提供任意类型和个数的参数,像上面的例子显式要求参数 NSString *bookIdNSInteger type
  3. Mediator可以做统一处理,调用某个组件方法时如果某个组件不存在,可以做相应操作,让调用者与组件间没有耦合。

到这里,基本上能解决我们的问题:各组件互不依赖,组件间调用只依赖中间件Mediator,Mediator不依赖其他组件。接下来就是优化这套写法,有两个优化点:

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

优化后就成了 casa 的方案,target-action 对应第一点,target就是class,action就是selector,通过一些规则简化动态调用。Category 对应第二点,每个组件写一个 Mediator 的 Category,让 Mediator 不至于太长。这里有个demo

总结起来就是,组件通过中间件通信,中间件通过 runtime 接口解耦,通过 target-action 简化写法,通过 category 感官上分离组件接口代码。

方案2

回到 Mediator 最初的三个问题,蘑菇街用的是另一种方式解决:注册表的方式,用URL表示接口,在模块启动时注册模块提供的接口,一个简化的实现:

//Mediator.m 中间件
@implementation Mediator
typedef void (^componentBlock) (id param);
@property (nonatomic, storng) NSMutableDictionary *cache
- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk {
 [cache setObject:blk forKey:urlPattern];
}

- (void)openURL:(NSString *)url withParam:(id)param {
 componentBlock blk = [cache objectForKey:url];
 if (blk) blk(param);
}
@end
//BookDetailComponent 组件
#import "Mediator.h"
#import "WRBookDetailViewController.h"
+ (void)initComponent {
 [[Mediator sharedInstance] registerURLPattern:@"weread://bookDetail" toHandler:^(NSDictionary *param) {
 WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:param[@"bookId"]];
 [[UIApplication sharedApplication].keyWindow.rootViewController.navigationController pushViewController:detailVC animated:YES];
 }];
}
//WRReadingViewController.m 调用者
//ReadingViewController.m
#import "Mediator.h"

+ (void)gotoDetail:(NSString *)bookId {
 [[Mediator sharedInstance] openURL:@"weread://bookDetail" withParam:@{@"bookId": bookId}];
}

这样同样做到每个模块间没有依赖,Mediator 也不依赖其他组件,不过这里不一样的一点是组件本身和调用者都依赖了Mediator,不过这不是重点,架构图还是跟方案1一样。

各个组件初始化时向 Mediator 注册对外提供的接口,Mediator 通过保存在内存的表去知道有哪些模块哪些接口,接口的形式是 URL->block

这里抛开URL的远程调用和本地调用混在一起导致的问题,先说只用于本地调用的情况,对于本地调用,URL只是一个表示组件的key,没有其他作用,这样做有三个问题:

  1. 需要有个地方列出各个组件里有什么 URL 接口可供调用。蘑菇街做了个后台专门管理。
  2. 每个组件都需要初始化,内存里需要保存一份表,组件多了会有内存问题。
  3. 参数的格式不明确,是个灵活的 dictionary,也需要有个地方可以查参数格式。

第二点没法解决,第一点和第三点可以跟前面那个方案一样,在 Mediator 每个组件暴露方法的转接口,然后使用起来就跟前面那种方式一样了。

抛开URL不说,这种方案跟方案1的共同思路就是:Mediator 不能直接去调用组件的方法,因为这样会产生依赖,那我就要通过其他方法去调用,也就是通过 字符串->方法 的映射去调用。runtime 接口的 className + selectorName -> IMP 是一种,注册表的 key -> block 是一种,而前一种是 OC 自带的特性,后一种需要内存维持一份注册表,这是不必要的。

现在说回 URL,组件化是不应该跟 URL 扯上关系的,因为组件对外提供的接口主要是模块间代码层面上的调用,我们先称为本地调用,而 URL 主要用于 APP 间通信,姑且称为远程调用。按常规思路者应该是对于远程调用,再加个中间层转发到本地调用,让这两者分开。那这里这两者混在一起有什么问题呢?

如果是 URL 的形式,那组件对外提供接口时就要同时考虑本地调用和远程调用两种情况,而远程调用有个限制,传递的参数类型有限制,只能传能被字符串化的数据,或者说只能传能被转成 json 的数据,像 UIImage 这类对象是不行的,所以如果组件接口要考虑远程调用,这里的参数就不能是这类非常规对象,接口的定义就受限了。

用理论的话来说就是,远程调用是本地调用的子集,这里混在一起导致组件只能提供子集功能,无法提供像方案1那样提供全集功能。所以这个方案是天生有缺陷的,对于遗漏的这部分功能,蘑菇街使用了另一种方案补全,请看方案3。

方案3

蘑菇街为了补全本地调用的功能,为组件多加了另一种方案,就是通过 protocol-class 注册表的方式。首先有一个新的中间件:

//ProtocolMediator.m 新中间件
@implementation ProtocolMediator
@property (nonatomic, storng) NSMutableDictionary *protocolCache
- (void)registerProtocol:(Protocol *)proto forClass:(Class)cls {
 NSMutableDictionary *protocolCache;
 [protocolCache setObject:cls forKey:NSStringFromProtocol(proto)];
}

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

然后有一个公共Protocol文件,定义了每一个组件对外提供的接口:

//ComponentProtocol.h
@protocol BookDetailComponentProtocol <NSObject>
- (UIViewController *)bookDetailController:(NSString *)bookId;
- (UIImage *)coverImageWithBookId:(NSString *)bookId;
@end

@protocol ReviewComponentProtocol <NSObject>
- (UIViewController *)ReviewController:(NSString *)bookId;
@end

再在模块里实现这些接口,并在初始化时调用 registerProtocol 注册。

//BookDetailComponent 组件
#import "ProtocolMediator.h"
#import "ComponentProtocol.h"
#import "WRBookDetailViewController.h"
+ (void)initComponent
{
 [[ProtocolMediator sharedInstance] registerProtocol:@protocol(BookDetailComponentProtocol) forClass:[self class];
}

- (UIViewController *)bookDetailController:(NSString *)bookId {
 WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:param[@"bookId"]];
 return detailVC;
}

- (UIImage *)coverImageWithBookId:(NSString *)bookId {
 ….
}

最后调用者通过 protocol 从 ProtocolMediator 拿到提供这些方法的 Class,再进行调用:

//WRReadingViewController.m 调用者
//ReadingViewController.m
#import "ProtocolMediator.h"
#import "ComponentProtocol.h"
+ (void)gotoDetail:(NSString *)bookId {
 Class cls = [[ProtocolMediator sharedInstance] classForProtocol:BookDetailComponentProtocol];
 id bookDetailComponent = [[cls alloc] init];
 UIViewController *vc = [bookDetailComponent bookDetailController:bookId];
 [[UIApplication sharedApplication].keyWindow.rootViewController.navigationController pushViewController:vc animated:YES];
}

这种思路有点绕,这个方案跟刚才两个最大的不同就是,它不是直接通过 Mediator 调用组件方法,而是通过 Mediator 拿到组件对象,再自行去调用组件方法。

结果就是组件方法的调用是分散在各地的,没有统一的入口,也就没法做组件不存在时的统一处理。组件1调用了组件2的方法,如果用前面两种方式,组件间是没有依赖的,组件1+Mediator可以单独抽离出来,只需要在Mediator里做好调用组件2方法时的异常处理就行。而这种方法组件1对组件2的调用分散在各个地方,没法做这些处理,在不修改组件1代码的情况下,组件1和组件2是分不开的。

当然你也可以在这上面跟方案1一样在 Mediator 对每一个组件接口 wrapper 一层,那这样这种方案跟方案1比除了更复杂点,其他没什么区别。

在 protocol-class 这个方案上,主要存在的问题就是分散调用导致耦合,另外实现上会有一些绕,其他就没什么了。casa 说的 “protocol对业务产生了侵入,且不符合黑盒模型。” 其实并没有这么夸张,实际上 protocol 对外提供组件方法,跟方案1在 Mediator wrapper 对外提供组件方法是差不多的。

最后

蘑菇街在一个项目里同时用了方案2和方案3两种方式,会让写组件的人不知所措,新增一个接口时不知道该用方案2的方式还是方案3的方式,可能这个在蘑菇街内部会通过一些文档规则去规范,但其实是没有必要的。可能是蘑菇街作为电商平台一开始就注重APP页面间跳转的概念,每个模块已经有一个对应的URL,于是组件化时自然想到通过URL的方式表示组件,后续发现URL方式的限制,于是加上方案3的方式,这也是正常的探索过程。

上面论述下方案1确实比方案2+方案3简单明了,没有 注册表常驻内存/参数传递限制/调用分散 这些缺点,方案1多做的一步是需要对所有组件方法进行一层 wrapper,但若想要明确提供组件的方法和参数类型,解耦统一处理,方案2和方案3同样需要多加这层。

实际上我没有组件化相关的实践,这里仅从 limboy 和 casa 提供的这几个方案对比分析,我还对组件化带来的收益是否大于组件化增加的成本这点存疑,相信真正实践起来还会碰到很多坑,继续探索中。

分类:技术文章 Tags:
评论

2016年3月19日 12:19

对于组件化使用url,我个人的理解也是更偏向于活动型app,这样运营人员有更多的自由来通过url操控页面活动。
另外有一个点,url的方式有个好处,就是可以更好支持灰度测试,因为url的方式完全可以通过携带参数来控制app到底改url应该是条native还是跳H5,万一某个native产生了一定的bug而无法轻易修复,那么完全可以通过切换返回H5页面地址,而不是native跳转地址。

2016年3月19日 14:02

组件化跟使用URL没什么关系,任意组件化方案都可以再加一层urlRouter解决url需求。方案2就是说直接用URL实现组件化(而不是组件化后再加一层)的话会有什么问题。

2016年3月19日 16:13

“Mediator可以做统一处理,调用某个组件方法时如果某个组件不存在,可以做相应操作,让调用者与组件间没有耦合。”

方案1里Mediator的wrapper当前只是调用runtime,并不能处理组件不存在的情况吧?要处理这个问题,需要在wrapper里统一调用某一个封装方法,如果我没有理解错的话?

2016年3月19日 16:33

wrapper里做什么都可以,这里示例只调了相应的runtime方法,实际上可以加上responseToSelector判断,转发调用,返回默认值之类的事

2016年3月19日 16:52

其实我的疑虑主要是”统一”。这部分逻辑按必须/可选依赖及业务需求等,应有不同处理。这样说,我就明白我没有误解什么了。

2016年7月5日 8:37

离青同学有没有相关见解的文章?希望可以拜读学习。

2016年3月20日 0:20

方案一中:
id obj = [[cls alloc] init];
这句的作用是几个意思?作者想让一个对象去执行一个类方法嘛?
至于蘑菇街为什么采用 url-block 的模式,应该和要和 安卓 统一架构便于运营有点关系吧^_^

2016年3月20日 9:53

写错了,已修正,多谢~这里类方法和实例方法都可以调

2016年3月21日 18:00

关于方案1,Mediator需要知道组件的名字,还要知道传哪些参数,怎么能说他们就是解耦了呢?
组件的实际使用者其实上也没有解耦,他还是需要知道组件的名字和参数列表。
他们只是没有了编译的依赖了而已,但业务逻辑上还是依赖的。
当组件改名或者参数换了后,Mediator不还是需要更新吗,使用者不也是要更新吗?
使用runtime避免了编译器的检查,岂不是让组件的重构更不安全?

2016年3月21日 19:33

不知道你对“解耦”的理解是什么,“组件的实际使用者其实上也没有解耦,他还是需要知道组件的名字和参数列表。”知道组件的名字和参数列表,跟有没有解耦是两回事……方案1组件只需要知道Mediator提供的名字和参数列表。如果连这个也不想知道,组件间怎样通信?

runtime除了让Mediator没有了编译依赖,还能在运行时判断去处理组件不存在的情况,所以Mediator是不依赖各个组件的,调用者和Mediator是可以单独抽出来用的。

任何方案都不能避免组件改了名字或参数后另一端需要修改,只不过这里另一端只是Mediator,而不是各个其他组件分散的调用。

组件重构是在接口不变的前提下进行的,这里没有影响。如果你是说组件修改对外接口时编译器检查不到,确实是,方案2使用注册的方式同样有这个问题。

2016年3月22日 18:00

hi,关于这个问题说起来很长,我写了篇博客 http://blog.pengqi.me/2016/03/22/ios-routing/ ,期待你的看法~

2016年3月22日 23:03

你的理解有问题,我该说的都说了

2016年3月23日 11:46

如果你能再思考下组件化的目的和意义,你会明白我的意思的,你自己不是也在本文末尾表示了怀疑吗?

> 我还对组件化带来的收益是否大于组件化增加的成本这点存疑,相信真正实践起来还会碰到很多坑

2016年3月23日 13:03

有同样的问题,加个Mediator只是用运行时去消除2个模块之间的import关系,但实际还是要知道被调模块接口的名字和参数,而这些东西都是String去转化了,如果接口或者参数改变, Mediator同样需要重写,而且这个Mediator会越来越大,如果使用分类,那么调用方要import Mediator的有关被调模块的分类,这和直接import被调模块有区别吗?
在我看使用Mediator的作用似乎是想让模块之间不互相import, 都去import Mediator或者Mediator的分类,可以这样理解吗?

2016年3月23日 14:44

当出现A,B,C三个VC相互之间调用的时候,引用调用关系是网状的了,@import就好多行了,但是采用Mediator的模式@import行数减少是直管的感受,引用调用的关系就变成星型网状图。对于业务人员添加自己VC的入口和寻找自己的跳转的入口都只需要遍历Mediator就好了。简单和复杂是矛盾统一的,这个自己去平衡了。

2016年3月23日 17:05

你想象一下像手Q淘宝那样几十个人做几十个功能模块该怎样协作就知道了。

2016年3月24日 14:59

真的是星型的吗?那这样的话你所有的模块都要依赖Mediator了, 而且随着项目的扩展, Mediator的规模可想而知, Mediator一改动那所有模块都要重新编译, 而且,每次有新模块或者新接口加进来,势必要动Mediator. 如果你用分类来减少Mediator的负担, 那请问, 你import分类的时候,再想想这个模块间的关系,还是星型吗?

2016年3月27日 12:35

作者有没有想过各个组件之间的数据复用怎么解耦,比如微信的联系人和朋友圈拆分开来为组件A和组件B,组件B(朋友圈)需要用到组件A的联系人数据(假设朋友圈动态数据没下发联系人数据,只下发了联系人id,需要去复用联系人的本地数据),在没有import组件A的数据model类的前提下,怎么使用组件A的数据。

2016年4月21日 14:49

需要复用的数据可以提供一个公用的数据管理类,需要取数据可以通过管理类来取。

[…] 2: 本文同时也参考了:http://blog.cnbang.net/tech/3080/ […]

2016年4月6日 11:00

不错,不错,看看了!

2016年4月6日 19:02

一直也是类似方法2的一个思路,使用注册的方式,这样不用去每次修改router,但是这样一来组件多了文件管理也会变得混乱,还需要一个更好的解决方案。

2016年4月14日 16:13

performSelector 会报一个警告performSelector may cause a leak because its selector is unknown,好像除了忽略掉之外没什么解决办法。。。

2016年7月5日 8:47

这个警告,如果你确认selector没问题的话,可以尝试

pragma clang diagnostic push

pragma clang diagnostic ignored “-Warc-performSelector-leaks”

[target performSelector:action withObject:params];

pragma clang diagnostic pop

2016年4月28日 12:48

1. 赞同应该叫『模块』而非『组件』。
2. Runtime动态调用和URL调用本质都是一样,都是用字符串去描述一个调用,去掉的只是编译依赖,而非逻辑依赖(上面例子确实也没有去逻辑依赖的必要),但是这种『字符串式去依赖』在我看来,有点为去依赖而去依赖,不如直接用JSPatch执行相应JS代码来得痛快。
3. 蘑菇街这种App用URL调度页面和组件,有一定合理性:1. 直观,运营人员也可以控制页面调转;2. 对于两端来说,都是一种简洁的调用描述;
4. 方案三太恶心了。

2016年5月16日 20:43

赞同,方案三不仅恶心,而且违反了Protocol的设计初衷

[…] iOS 组件化方案探索 […]

2016年5月12日 15:40

感觉说的更像页面跳转方案,举的栗子也是UIViewControlle之间跳转以及如何传值的问题。对于一些非页面的“组件”,比如下拉刷新,加载转圈等还是需要直接依赖的。

2016年5月18日 18:24

组件化突然火了。。有企业版应用的可以戳进来看看,基于Cocoa Touch Framework、可以独立更新的才叫组件化。:)
https://github.com/wequick/Small

2016年7月1日 15:56

感觉这些代码你没有敲一遍呢,我一路跟过来,踩了好多坑
比如在+initComponent方法里,你怎么调用了self.navgationController….

2016年7月2日 22:47

没有敲过,直接在文章写的,确实有些写错了,多谢指出,已改正。知道意思就行了~

2016年7月4日 16:20

因为这个套路没有能很好的理清楚,所以就一步步地敲过来理解,有些地方按照文章的思路过不去了才提出来的。

2016年7月5日 8:35

感谢bang的分析,帮我理清了一些东西,不过问题应该还不止于此,我还是先继续探索吧。

2016年7月4日 15:36

不要在意细节,感写博主写的挺好的了,你还想直接copy?

2016年7月4日 16:10

正是因为博主没敲过,又怎么经得起推敲?

2016年7月4日 16:11

就刚才这个例子,如果拿不到viewController又怎么push出去,只有敲才能看到问题好吧

2016年7月4日 19:31

怎样push你可以看看现在修改后的。
其实这都不是重点。

2016年7月29日 15:49

这个真的不是重点哈~
楼主总结的很到位,解决了我不少困惑

2016年7月19日 10:13

我们也有一些组件化策略,类似方案3,模块需要实现的协议方法只有一个launch,不过调起参数基本都是服务器给的
http://imrazor.github.io/blog/2016/04/21/componentization/

2016年8月26日 15:49

看着两位大神撕逼,而一直不得其解, bang大神的非常通俗易懂,终于明白他们在说什么了

2016年12月14日 16:39

博主写得非常不错,3Q!

2016年12月26日 18:07

但是对于有些App需要讲究动态化,页面跳转采用Url走配置的方式比较动态化,单纯通过Runtime方式我觉得是不可能实现动态化的,因为代码都写死的,也是要通过配置或者接口来实现动态化的。我们项目对于组件间的服务和组件生命周期管理采用BeeHive,对于页面跳转采用Url跳转。这两种方式混合使用来达到组件化+动态化是否是上解呢?

[…] iOS 组件化方案探索 […]

2016年12月28日 17:09

大神问一下,这种组件化,在组件A通过方案1调用组件2后,组件2如果有状态要去反馈组件1该怎么弄呢?

2017年1月12日 11:46

这样的组件化方案,基础组件应该怎么办呢?业务组件要怎样使用像UI、网络这些基础组件?直接依赖还是有什么其它方法?求解惑。

2017年2月15日 22:58

谢谢楼主 又涨姿势了

2017年2月23日 2:10

[…] 回到组件化的技术方案,最早是Limboy分享了一篇蘑菇街组件化的技术方案,接着Casa提出了不同意见,后来Limboy在Casa反馈之上对自己方案做了进一步优化,最后Bang在前三篇文章基础之上做了清晰的梳理总结。通读之后,获益颇多,组件化所面临的问题,和可能的解决思路也变得更清晰。 […]

2017年3月13日 11:31

个人认为二者所关注的重心不同。
使用URL是为了解决页面跳转问题,也就是页面级解耦,实现统跳。
protocol-class方案实现依赖解耦和工程解耦,解决模块之间的强依赖关系,对外提供protocol,解决模块强依赖带来的开发阶段痛苦。

[…] 2016.03.18 iOS 组件化方案探索 […]

2017年3月30日 18:09

大神,远程调用是不是之app之间跳转

2017年4月23日 18:49

讲解的很透彻,后两种的缺点确实是,组件化的重点在于利用中介者模式。

2017年4月23日 18:53

文章通俗易懂,后两者的缺点确实是!重点在于利用中介者模式。

2017年6月8日 20:40

bang神很风趣👍

2017年6月20日 16:53

我觉得组件之间在用之前还是要定义好规则的,有的组件可以独立使用,有的不行。比方说需要认证的,必须经过其他组件才能调用他。照这样的话,其实我们真正能暴露的一级组件会减少很多,而有些是不必要暴露的。所以我们也可以把这些组件分类成相互独立的模块,然后接着分层定义不同的mediator 然后还是要有一个总的mediator(想了半天好像这个还真不能不要)。还有组件间数据传递还是字典或者json比较好,这样避免的url的麻烦,然后再也不用写烦人的model了,保持组件间的尽量独立。这样增加,删除以及搬运到不同的地方都很方便。而且我们也不需要分别外部和内部调用,因为对app来说不管是其他app,还是用户操作其实都能理解为外部调用。而且还是倾向定义规则,不能让组件无条件的调用。其实很像一台服务器。仅仅是说说自己的理解。。。哈哈。

2017年7月13日 19:57

[self WillPresentViewController:@”VC2″ animated:YES frame:CGRectMake(hScreenWidth-150, 74, 146, 165) style:TTPresentStyleCustomModal_blackhud SetupParms:^(UIViewController *vc, NSMutableDictionary *dict) {

CGRect rect = CGRectMake(5, 5, 5, 5);

[dict addEntriesFromDictionary:@{
@”test1″:@”传参数啦”,
@”testint”:@”123″,
@”testrect”:[NSValue valueWithCGRect:rect]
}];
} completion:^{

} callback:^(id parameter) {

} jumpError:^{

UILabel *lb = [[UILabel alloc] initWithFrame:CGRectMake(20, 100, 350, 40)];
lb.font = [UIFont systemFontOfSize:13];
lb.text =[NSString stringWithFormat:@”VC2模块,不在该项目中….”];
lb.backgroundColor = [UIColor blueColor];
lb.textColor = [UIColor whiteColor];

lb.layer.cornerRadius = 5;
lb.layer.masksToBounds = YES;

// view.backgroundColor = randomColor;
[[UIApplication sharedApplication].keyWindow addSubview:lb];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[lb removeFromSuperview];
});

}];

2017年7月13日 20:01

工具类解耦

TTMethodRouterObject(@”BSSCAppUser”).TTMethodName(@”-(void)searchUserWithusername:(NSString*)username Success:(httpRequestSuccess)success Failure:(httpRequestFailure)failure;”).TTParmsName(@”13471045617″,^(id parms1,…){

},^(id parms1,…){

});

2017年11月18日 11:21

Hi bang, 你说的

“如果是 URL 的形式,那组件对外提供接口时就要同时考虑本地调用和远程调用两种情况,而远程调用有个限制,传递的参数类型有限制,只能传能被字符串化的数据,或者说只能传能被转成 json 的数据,像 UIImage 这类对象是不行的,所以如果组件接口要考虑远程调用,这里的参数就不能是这类非常规对象,接口的定义就受限了。
用理论的话来说就是,远程调用是本地调用的子集,这里混在一起导致组件只能提供子集功能,无法提供像方案1那样提供全集功能。所以这个方案是天生有缺陷的,对于遗漏的这部分功能,蘑菇街使用了另一种方案补全,请看方案3。”

远程调用的话无论哪种方案都只能传能被字符串化的数据吧 ? 方案一应该也无法远程传UIImage这种吧 ?

2017年11月23日 10:58

阅读了大神的文章,受益匪浅,实践中还有几个不太明白的,关于Mediator ,方案一,1.假如参数是UIImage怎么传递。2 如果传的是model是不是转化为字典传递过去之后再转化为model. 3 要是A到B模块传递。A模块的传递的Block怎么传回来。

2017年12月14日 0:27

[…] 腾讯bang方案 […]

2018年1月28日 22:17

常驻内存的问题,可以把相关类的注册仅在调试模式时使用,生产包关闭掉。不规范的该蹦还蹦。

2018年2月10日 18:21

非常感谢博主,解决了不少困惑,虽然很早就接触过组件化,但是一直没能实践,最近打算参考一些案例自己弄一套。看到评论里面的一些质疑,其实我想说的是,懂的人自然会动。

2018年2月28日 13:36

针对方案一的Target-Action解决方案,是否可以将组件和标识写在plist文件里面。统一读取再通过runtime反向映射,甚至组件内的init方法也可以写在plist文件中通过selector动态调用。demo如下:https://github.com/MrTung/MTRouter。

2018年4月12日 10:57

组件化这个问题到了18年已经冷却不少了。
不过对比过这种方案, 感觉还是觉得都不够完善。也许之前各种鼓吹『组件化』的app, 大部分都是电商类的, 各种独立功能打包成一个大杂烩app。
如果是一个app, 根据功能可以分成多个业务模块, 但是各个模块之间, 又有UI(VC)和数据的交互, 这个应该怎么做?
举个例子:
比如微信读书,如果有个图书管理模块, 里边有各种图书的数据信息, 还有图书展示,阅读界面。
然后有另外一个模块, 如果需要查询某本书的信息(并非UI跳转), 那么这种方式, 组件化应该怎么设计?

2018年7月24日 11:50

通过协议实现组件化解耦的直播demo。
https://github.com/mlcldh/LCLive

2018年9月27日 11:35

文章写得通俗易懂,非常感谢

2018年9月27日 11:37

文章写得很好,我这种弱鸡都看懂了,非常感谢

[…] iOS组件化实践方案-LDBusMediator炼就 iOS组件化思路-大神博客研读和思考 iOS 组件化方案探索 蘑菇街 App 的组件化之路 […]

2018年10月12日 2:54

[…] iOS组件化实践方案-LDBusMediator炼就 iOS组件化思路-大神博客研读和思考 iOS 组件化方案探索 蘑菇街 App 的组件化之路 […]

[…] iOS 组件化方案探索 […]

[…] iOS 组件化方案探索 […]

2019年8月26日 9:04

[…] 看完 Limboy(蘑菇街 App 的组件化之路) 和 Casa(iOS应用架构谈 组件化方案) 关于 iOS 上模块化的讨论,以及 Bang(iOS 组件化方案探索)的总结,整理一下。 […]

[…] bang – iOS 组件化方案探索 […]

[…] bang – iOS 組件化方案探索” […]

[…] iOS 组件化方案探索 […]

[…] iOS 组件化方案探索 […]

[…] web components service-oriented architecture function as a service bang – iOS 组件化方案探索 domain model domain driven design package principles modular programming separation of concerns […]

2022年11月14日 13:30

[…] 2016.03.18 iOS 组件化方案探索 […]