唐山APP开发,唐山APP制作,唐山APP定制开发-唐山小程序开发
盛秋网络微信公众号 扫一扫关注
tel-icon全国服务热线: 0316-2636468  13831639196
扫一扫关注盛秋网络微信公众号

联系我们

盛秋网络科技(唐山)有限公司

  • 电话:13831639196
  •  0316-2636468
  • 地址:唐山市路北区体育馆道25号硅谷大厦
  • 网址:www.tangshanapp.cn

最新资讯

您现在的位置: 首页 > 新闻资讯 > 最新资讯

iOS组件化方案

发布日期:2018年09月05日    浏览次数:911

前言

看了一些关于组件化文章,决定写篇文章稍稍做些总结。

一、组件化的误解

首先笔者认为组件化这个词用的不合适,应该改为模块化。按照笔者的理解组件通常是指比较小的功能模块,比如在RN中,组件(component)通常就相当于 iOS 开发中的视图模块,如tabBar、navBar等。而模块通常是指较大粒度的业务模块,比如一个商城类项目通常会有登录模块、购物车模块、清单模块模块等。为了下文不产生歧义,下面模块和组件代表同一个意思,都是指较大颗粒度的业务模块。

二、为什么要组件化

随着公司业务的不断发展,应用的代码体积将会越来越大,业务代码耦合也越来越多,代码量也是急剧增加。如果仅仅完成代码拆分还不足以解决业务之间的代码耦合,而组件化是一种能够解决代码耦合、业务工程能够独立运行的技术。

三、组件化实现流程

在实施组件化之前首先要意识到,并不是所有项目都适合组件化。首先刚起步的项目可能模块不是十分清晰,上来就实施模块化方案,很有可能对后期代码维护或功能扩展带来很多不便之处;其次,模块化更适合大型项目且是多人开发,如果项目比较小且开发者较少,使用组件化可能只会带来更大的工作量。

3.1 使用 pod 管理公共库和UI组件

封装公共库和项目中的UI组件库,然后制作成私有化仓库,通过 pod 在实际项目中使用。另外针对一些第三方库,要在第三库的基础上再做一层封装,这样后期可以更方便的替换这些第三方库。

3.2 拆分业务模块

对一些独立的模块进行拆分,如登录模块、购物车模块、清单模块、商品详情模块等。实际拆分的过程中需要注意,模块的颗粒度既不能太大,也不能太小。

3.3 实施组件化方案

抽出公共库和UI组件以及拆分完业务模块之后,接下来就是实施组件化方案。关于组件化方案笔者主要看了蘑菇街和casa的方案,总结如下。

四、蘑菇街url-block方案

蘑菇街最初采用的是 URL 跳转模式。如下代码,启动时通过MGJRouter 注册组件提供的服务,把调用组件使用的URL和组件提供的服务block对应起来,保存到内存中。在使用组件的服务时,通过URL找到对应的block,然后调用对应block中的服务。

//注册

[MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) {

    NSNumber *id = routerParameters[@"id"];

    // create view controller with id

    // push view controller

}];

//调用[MGJRouter openURL:@"mgj://detail?id=404"];

再具体点,就可以看下面这个例子。触发WRReadingViewController类中的+ (void)gotoDetail:(NSString *)bookId方法,展示WRBookDetailViewController界面。其中的Mediator就可以理解为类似MGJRouter的中间媒介。Mediator中的cache属性就可以理解为上述所说的URL和block的映射表。

//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}];

}


url-block方案具有非常明显的几个缺点:

1、组件本身和调用者都依赖了Mediator,耦合度较大。

2、内存里需要保存一份url-block映射表,增加了额外的内存。

3、非常规对象在组件间无法进行参数传递,因为实际参数传递通过URL传递,只能传递常规的字符串参数,无法传递类似UIImage、NSData等类型。

4、没有拆分远程调用和本地间调用,本地调用和远程调用不应该公用同一个接口,不应该以远程调用的方式为本地间调用提供服务。远程App调用处理入参的过程比本地多了一个URL解析的过程,这是远程App调用特有的过程。而本地完全可以避免引入URL解析这一步骤,直接调用。

五、蘑菇街protocol-class方案

由于前面的url-block方案不能够传递非常规参数,因此有了第二种方案protocol-class。

//注册[ModuleManager registerClass:ClassA forProtocol:ProtocolA];

调用

[ModuleManager classForProtocol:ProtocolA];

这种方案实际上同url-block方案非常类似,同样需要中间件维护一个映射表/字典,该映射表/字典主要用来维护protocol和class的关系。该方案主要解决了url-block方案中的非常规参数不能传递的问题,但是对于组件依赖中间件、内存中维护映射表等问题依然没有给与解答。

六、casatarget-action方案

上述两个方案都存在很大的问题,接下来重点看casa给出的target-action方案,相对于前面两种方案而言,该方案比较好。case在文章中长篇大论说了不少蘑菇街方案的弊端,以及自己这种方案的好处。总的来说该方案是先封装一个中间层,其中中间件分别提供了本地调用和远程调用接口。对于组件而言,每个组件会包装一层。当需要调用组件的时候,就会通过中间层调用各个组件的包装层,比较特别的地方是中间层通过runtime调用组件的包装层,做到真正意义上的解耦,这也是该方案的核心之处。

结合实际代码简单看一下该方案的实现。以下代码来自casa的组件化demo。

组件A

可以理解为下面的DemoModuleADetailViewController类

组件A的包装层(Target)

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

- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params

{

    // 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明

    DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];

    viewController.valueLabel.text = params[@"key"];

    return viewController;

}


中间层


+ (instancetype)sharedInstance;

// 远程App调用入口

- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;

// 本地组件调用入口

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;

- (void)releaseCachedTargetWithTargetName:(NSString *)targetName;


中间层针对组件A接口的分类


// CTMediator+CTMediatorModuleAActions.h

- (UIViewController *)CTMediator_viewControllerForDetail;


// CTMediator+CTMediatorModuleAActions.m

- (UIViewController *)CTMediator_viewControllerForDetail

{

    return [self performTarget:kCTMediatorTargetA action:kCTMediatorActionNativFetchDetailViewController params:@{@"key":@"value"} shouldCacheTarget:NO];

}


调用


// ViewController.h

#import "CTMediator+CTMediatorModuleAActions.h"

[self presentViewController:[[CTMediator sharedInstance] CTMediator_viewControllerForDetail] animated:YES completion:nil];


如果想使用组件,调用者只需要依赖中间层即可,而中间层通过target-action模式无需依赖组件,所以达到解耦的目的。


中间层CTMediator将远程调用和本地组件间调用拆开处理。之所以这样做,主要因为远程App调用处理入参的过程比本地多了一个URL解析的过程,这是远程App调用特有的过程,而本地调用无需URL解析。


该方案中采用了去model化传递参数,在iOS的开发中,就是以字典的方式去传递参数。如果组件间调用不对参数做去model化的设计,就会导致业务形式上被组件化了,实质上依然没有被独立。既然是使用了字典作为参数传递,自然而然就引起了hardcode问题。为了让调用更方便知道接收方需要哪些key的参数以及哪些target可以被调用,该方案进一步就针对每一模块采用了category的方式,从而缩小了范围,方便代码定位和阅读。


总结


以上简单分析了蘑菇街url-block方案、蘑菇街protocol-class以及case的target-action方案,分析的实际很浅。其实笔者在实际开发工作中完全没有接触过组件化开发,只是对组件化比较感兴趣,看了些文章后,简单做一些总结。


运行0.05286秒,内存使用735.09 KB,数据库执行51次,用时0.02197秒,缓存执行20次,用时0.00343秒