iOS开发的组件化方案的文章介绍已经不少了,可是不多有能介绍如何在项目工程中进行实施的,本文则是做者在实际项目中实施组件化方案后总结的一些经验。本文不会讨论太多理论上的知识,主要集中在实施方面。html
实施业务组件化是将每个业务模块单独封装成pods,而后在主工程中经过CocoaPods以组件的方式将全部模块集成进来。组件化的实施须要依赖Git和CocoaPods进行,因此在开始以前须要在macOS上安装好Git和CocoaPods,同时准备好一个Git服务器。本文使用Github为做为例子,使用其它Git服务操做步骤不会有太大的差别。git
实际开发中,每个业务模块对应一个Git仓库,每个业务Git仓库对应一个pods,建议将全部仓库放在一个组织(organization)中。如图所示,在Github中建立一个组织。github
建立完成以后就能够在组织中建立你的业务模块仓库以及邀请你的开发小伙伴进入组织。objective-c
CocoaPods有个默认公共开放的pods仓库,里面存放了不少开源的iOS组件库供开发者使用,并存放在Github上。可是开发公司项目,代码不会对外开放,因此只能使用私有pods,那么就须要自建私有pods仓库来存放这些私有pods。如图所示,在组织中建立一个空仓库。bash
仓库建立完成以后,须要添加一个本地私有pods仓库并连接到该远程Git仓库。打开macOS的命令行,输入:服务器
pod repo add ModularizationPod https://github.com/iOSShop/ModularizationPod.git
复制代码
完成后咱们进入到CocoaPods的目录下:网络
cd ~/.cocoapods/repos/
open .
复制代码
能够看到目录中看到两个仓库,master是CocoaPods的公共pods仓库,ModularizationPod则是咱们建立的私有pods仓库。架构
下一步咱们在ModularizationPod仓库中添加一个.gitignore文件,能够直接从master的仓库里面复制一个过去。框架
在命令行终端中cd到仓库目录:ide
cd /Users/caicai/.cocoapods/repos/ModularizationPod
复制代码
而后咱们须要提交到远程Git仓库:
git add .
git commit -m "first commit"
git push
复制代码
中间可能须要输入帐号密码进行Git的身份校验,完成后能够在远程Git仓库上查看。
之后这个远程Git仓库会存放咱们业务模块的pods信息。
本文以一个简单的商城业务来描述组件化方案实施,内容包括如何实现业务模块的组件化、如何进行模块间的调用以及如何进行模块之间通讯。咱们以基础的帐户、商品、订单、支付这四个模块进行举例。
实现业务模块的组件化,是为了将业务拆分出来,下降业务模块之间的耦合性。好比在商品详情页面【点击购买】后下一步就是进入订单生成页面,传统的作法就是直接在商品详情页面的ViewController里面直接import订单生成页面的ViewController,而后实例化ViewController传个值后直接push过去就能够了。当项目规模变大和业务逻辑变复杂的时候,这种直接引入代码文件的作法就会使得模块之间的依赖变得愈来愈强,甚至是牵一发就动全身。即便举例的只有四个模块,相互间的依赖也比较多,以下图所示:
其实这个问题属于通用的软件工程,不局限于iOS开发中。解决的办法也很简单,提供一个中间人(Mediator)。业务模块之间不直接进行引用,经过Mediator间接造成引用关系,并且在Mediator能够将模块须要暴露出来的业务提供出来给其它模块调用,不须要暴露出来的就不引入Mediator。好比帐户模块有登录页面和注册页面,实际场景中可能只会把登录页面给其余业务模块调用,注册页面只须要从登录页面跳转过去就能够了,并不须要提供给其它业务模块调用。以下图所示:
经过中间人的方式拆分业务模块后也只是逻辑上清晰了一点,实际上仍是在引入业务的代码文件,业务与业务之间的调用依旧很不清晰明了。好比在订单页面弹出一个登录页面,订单模块的开发人员须要先找到登录页面的UIViewController文件,而后import进来,接着实例化对象,最后再present或者push这个页面。复杂一点的业务可能还须要以口头或者文档的形式告知调用方如何去使用类文件、如何去传递参数等等。而开发人员想着我只须要一个UIViewController实例化对象就能够了,也不关心它是哪一个代码文件、它内部是怎么实现的。
咱们能够经过服务的方式去解决这个问题,简单的说就是你须要什么,我给你什么。经过Target-Action,业务提供方将全部的服务以对象方法的形式提供,经过方法的参数和返回值进行模块间的调用和通讯。以下图所示:
完成前两步以后,还存在两个问题:
第一个问题的解决办法,经过组合的思想,使用Objective-C的分类(Category)将Mediator去中心化。针对每一个业务模块建立一个Mediator的分类(Category),并将Target服务引入到分类(Category)中,至关于将Target服务再作一层方法封装,其余业务调用方只需引入相应的分类(Category)便可,这样就能够避免无关业务服务的多余引入。同时业务模块对外提供的服务修改后,相应的业务提供方只需修改本身的分类(Category)便可,Mediator也无需维护,达到真正的去中心化。以下图所示:
经过上图能够看到模块与模块之间的调用已经没有直接引入了,都是经过Category引入。在上图的基础上仍是能够看到依赖并无减小,Category会引用Target-Action,并间接引用源代码文件。
第二个问题的其实就是Category与Target-Action之间的依赖问题,解决办法也很简单粗暴。由于业务模块中对外提供服务的Category中的方法实现其实就是直接调用的Target类里面的Action方法,因此经过runtime的技术就能够直接切段二者之间的依赖。
经过以上方法,Category能够不用import就直接调用Target-Action的服务,并传递出去,这样就完成了解除依赖。以下图所示:
至此,业务架构设计就很是清晰明了。以上就是组件化工程实施的方案。
新建一个目录ModularizationProject用于存放全部工程实施的文件,而后在ModularizationProject下新建ConfigPods目录,用于存放一些配置文件,目录及文件结构以下图所示:
templates目录下的文件都是帮助建立Xcode工程的,经过config.sh的脚本能够快速建立工程并进行私有pods的配置。查看示例
gitignore能够在Git进行提交时对文件过滤。
readme.md能够对Git仓库进行一些描述说明,按须要撰写便可。
Podfile是建立cocoapods工程时必须的文件,示例文件里面第一个source开头后面的地址是私有pods仓库的远程Git仓库地址,改为本身的便可。
pod.podspec是将工程打包成pods的必要配置文件,里面内容可按需修改。使用示例文件,建议只修改s.author后面的信息就能够了。
upload.sh里面是打包pods的命令,示例文件里面push后面是私有pods仓库名,--sources后面的参数中第一个是私有pods仓库的远程Git仓库地址,两个都须要改为本身的。
config.sh是建立整个工程的脚本,建议不作修改直接使用示例文件。
在Git组织中建立帐户模块的远程仓库。
在ModularizationProject中建立一个名为AccountModule的iOS工程,注意建立过程当中,Source Control不要勾选。
打开终端命令行cd到config.sh所在的目录,而后执行
./config.sh
复制代码
Enter Project Name:输入工程的名字
AccountModule
Enter HTTPS Repo URL:输入工程的远程Git仓库的https地址
Enter SSH Repo URL:输入工程的远程Git仓库的地址
git@github.com:iOSShop/AccountModule.git
Enter Home Page URL:输入工程的主页:
confirm:核对以上输入的信息
y
帐户模块的远程Git仓库就建立完毕,并完成了第一次初始化的提交。
进入本地的AccountModule目录中,而后执行
pod install
复制代码
完成后,帐户模块的cocoapods工程就建立完毕,并能够进行开发了。并且工程中自带资格同名目录,将全部代码文件放在该目录便可。
以上步骤通用于建立业务模块工程。
在进入开发阶段前,须要对各个业务模块的职责进行划分,并规则好各个业务模块须要对外提供的服务,因此咱们能够先完成业务模块中大部分Target-Action和对应的Category的编写。拿帐户模块来举例,其余业务模块可能须要登录页面、用户的登陆状态、用户登陆状态的改变。
帐户模块Target-Action中的方法声明:
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface Target_Account : NSObject
/**
*登陆
**/
- (UIViewController *)Action_nativeLoginViewController;
/**
*登录状态
**/
- (BOOL)Action_nativeLoginStatus;
/**
*登录状态改变
**/
- (NSString *)Action_nativeLoginStatusChangeNotificationName;
@end
NS_ASSUME_NONNULL_END
复制代码
Mediator思想的实现来源于CTMediator,核心只有两个文件就已经能知足大部分的使用场景。在实际项目开发中可直接依赖该框架,也能够clone下来后按照须要进行修改。示例中对其进行修改后建立了新的CCMediator,并制做成私有pods库供使用。
按照3.2的完整步骤建立一个名为AccountModule_Category的工程,而后再进入AccountModule_Category工程中,编辑Podfile,加入pod 'CCMediator',而后再pod install。
建立CTMediator的Category,下面是Category中方法的声明和实现
#import "CCMediator.h"
NS_ASSUME_NONNULL_BEGIN
@interface CCMediator (AccountModule)
/**
*登录(presentViewController)
**/
- (UIViewController *)Account_viewControllerForLogin;
/**
*登录状态
**/
- (BOOL)Account_statusForLogin;
/**
*登录状态改变
**/
- (NSString *)Account_nameForLoginStatusChangeNotification;
@end
NS_ASSUME_NONNULL_END
复制代码
#import "CCMediator+AccountModule.h"
NSString * const MediatorTargetAccount = @"Account";
NSString * const MediatorActionAccountLoginViewController = @"nativeLoginViewController";
NSString * const MediatorActionAccountLoginStatus = @"nativeLoginStatus";
NSString * const MediatorActionAccountLoginStatusChangeNotification = @"nativeLoginStatusChangeNotificationName";
@implementation CCMediator (AccountModule)
/**
*登录(presentViewController)
**/
- (UIViewController *)Account_viewControllerForLogin {
UIViewController *viewController = [self performTarget:MediatorTargetAccount action:MediatorActionAccountLoginViewController params:nil shouldCacheTarget:NO];
if ([viewController isKindOfClass:[UIViewController class]]) {
return viewController;
} else {
return [[UIViewController alloc] init];
}
}
/**
*登录状态
**/
- (BOOL)Account_statusForLogin {
return [[self performTarget:MediatorTargetAccount action:MediatorActionAccountLoginStatus params:nil shouldCacheTarget:NO] boolValue];
}
/**
*登录状态改变
**/
- (NSString *)Account_nameForLoginStatusChangeNotification {
return [self performTarget:MediatorTargetAccount action:MediatorActionAccountLoginStatusChangeNotification params:nil shouldCacheTarget:NO];
}
@end
复制代码
经过Category完成服务传递,同时在Mediator中解决了Category与Target-Action之间的依赖。
完成了Category和Target-Action的编写,就能够经过Git提交到远程仓库并生成pods供其它业务模块引用。
步骤以下:
编辑podspec文件,修改s.version的版本,而后针对资源和依赖进行自定义设置,podspec的详细用法可参考官方指导。
打开终端cd到工程目录下,开始提交代码。
git add .
git commit -m "add Target-Action"
git push
复制代码
打标签,制做并推送私有pods。tag须要与podspec的s.version保持一致,而后执行目录下的upload.sh脚本。执行过程当中可能报错,必定要按照提示去解决。
git tag 1.0.0
git push --tags
./upload.sh
复制代码
制做完成后能够在本地的pods仓库和远程Git仓库中看到被推送的pods信息。
下面是经过pod search查找的结果:
其它业务模块能够直接在其工程的Podfile里面集成AccountModule_Category和AccountModule就能够调用帐户模块的服务了。
咱们以商品模块为例,进入在【个人商品界面】后,须要在该页面判断用户是否登录了,没有登录则提示登录并能跳转到登录页面,而且还要实时监听用户登录状态的改变。
实现用户状态的监听
NSString *notificationName = [[CCMediator sharedInstance] Account_nameForLoginStatusChangeNotification];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(loginStatusChange) name:notificationName object:nil];
复制代码
实现监听的方法,当用户登录时就隐藏提示登录的页面,当用户未登录时显示提示登录的页面。
- (void)loginStatusChange {
BOOL isLogin = [[CCMediator sharedInstance] Account_statusForLogin];
self.loginView.hidden = isLogin;
if (isLogin) {
[self.view bringSubviewToFront:self.loginView];
}
}
复制代码
响应提示登录的操做,弹出登录页面
- (void)clickLogin {
UIViewController *viewController = [[CCMediator sharedInstance] Account_viewControllerForLogin];
[self presentViewController:[[UINavigationController alloc] initWithRootViewController:viewController] animated:YES completion:nil];
}
复制代码
以上是一些基本的服务调用方式,能知足大部分模块间的调用场景。其余类型的服务可自行思考如何处理,须要注意的是方法的返回值类型必定要是基本数据类型和常规对象。这里的常规对象指的是Foundation框架、UIKit框架或者其它一些系统库框架中的对象。若是使用的是自定义对象作返回值,带来的将是强耦合关系。
不一样的业务模块之间进行调用时确定免不了须要通讯,好比从商品详情页面跳转到订单生成页面,商品详情页面在调用订单生成页面时须要传递参数至少包括商品id和商品数量。那么订单生成的Category方法声明以下:
#import "CCMediator.h"
NS_ASSUME_NONNULL_BEGIN
@interface CCMediator (OrderModule)
/**
*生成订单
**/
- (UIViewController *)Order_viewControllerForMakeWithGoodsID:(NSNumber *)goodsID goodsCount:(NSInteger)goodsCount;
@end
NS_ASSUME_NONNULL_END
复制代码
Category全部传递的参数都封装到一个NSDictonary中,而后传递给对应的Target-Action。Category方法实现以下:
#import "CCMediator+OrderModule.h"
NSString * const MediatorTargetOrder = @"Order";
NSString * const MediatorActionOrderMakeViewController = @"nativeOrderMakeViewController";
@implementation CCMediator (OrderModule)
/**
*生成订单
**/
- (UIViewController *)Order_viewControllerForMakeWithGoodsID:(NSNumber *)goodsID goodsCount:(NSInteger)goodsCount {
if (goodsID == nil) {
NSException *exception = [[NSException alloc] initWithName:@"Order_viewControllerForMakeWithGoodsID:goodsCount:提示" reason:@"goodsID不能为空" userInfo:nil];
@throw exception;
}
if (goodsCount < 1) {
NSException *exception = [[NSException alloc] initWithName:@"Order_viewControllerForMakeWithGoodsID:goodsCount:提示" reason:@"goodsCount错误" userInfo:nil];
@throw exception;
}
NSMutableDictionary *params = [NSMutableDictionary dictionary];
params[@"goodsCount"] = [NSNumber numberWithInteger:goodsCount];
params[@"goodsID"] = goodsID;
UIViewController *viewController = [self performTarget:MediatorTargetOrder action:MediatorActionOrderMakeViewController params:params shouldCacheTarget:NO];
if ([viewController isKindOfClass:[UIViewController class]]) {
return viewController;
} else {
return [[UIViewController alloc] init];
}
}
@end
复制代码
若是参数是必要的,能够在传递到Target-Action以前进行检测,不符合要求能够直接抛出异常。固然也能够根据产品的须要进行自定义处理。那么对应的Target-Action方法声明则是:
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface Target_Order : NSObject
/**
*生成订单
**/
- (UIViewController *)Action_nativeOrderMakeViewControllerWithParams:(NSDictionary *)params;
@end
NS_ASSUME_NONNULL_END
复制代码
带参数和不带参数的方法声明多了一个WithParams,具体能够看CCMediator中的实现。对应的Target-Action方法实现则是:
#import "Target_Order.h"
#import "OrderMakeViewController.h"
@implementation Target_Order
/**
*生成订单
**/
- (UIViewController *)Action_nativeOrderMakeViewControllerWithParams:(NSDictionary *)params {
OrderMakeViewController *orderViewController = [[OrderMakeViewController alloc] init];
orderViewController.goodsCount = [params[@"goodsCount"] integerValue];
orderViewController.goodsID = params[@"goodsID"];
return orderViewController;
}
@end
复制代码
为何使用NSDictionary传递参数,由于它是个容器,属于Foundation框架中的类,使用它不会形成Category和Target-Action间产生依赖,能够把全部的参数统一封装起来进行传递。并且模块之间的参数传递应该尽量少,不然会使模块间的耦合性加强。同时传递的参数也必须是基本数据类型和常规对象,不要传递自定义对象。
在商品详情页面调用订单生成页面
- (void)clickBuy {
UIViewController *viewController = [[CCMediator sharedInstance] Order_viewControllerForMakeWithGoodsID:self.goodsID goodsCount:99];
[self.navigationController pushViewController:viewController animated:YES];
}
复制代码
上面描述了跨模块的通讯,可是示例是正向的传参,如何实现逆向传参呢。例如常见的场景,我从A页面到B页面,B页面作了一些操做后把一些参数传递给A。实现的办法就是使用block,将block封装到NSDictonary而后传递过去就能够实现。示例场景中商品详情页面进入订单生成页面完成付款后返回成功的信息给商品详情页面进行显示,以下图所示:
如今订单模块的Category的方法声明修改以下:
#import "CCMediator.h"
NS_ASSUME_NONNULL_BEGIN
typedef void(^SuccessBlock)(NSString *);
@interface CCMediator (OrderModule)
/**
*生成订单
**/
- (UIViewController *)Order_viewControllerForMakeWithGoodsID:(NSNumber *)goodsID goodsCount:(NSInteger)goodsCount success:(SuccessBlock)successBlock;
@end
NS_ASSUME_NONNULL_END
复制代码
Category的方法实现修改以下:
#import "CCMediator+OrderModule.h"
NSString * const MediatorTargetOrder = @"Order";
NSString * const MediatorActionOrderMakeViewController = @"nativeOrderMakeViewController";
@implementation CCMediator (OrderModule)
/**
*生成订单
**/
- (UIViewController *)Order_viewControllerForMakeWithGoodsID:(NSNumber *)goodsID goodsCount:(NSInteger)goodsCount success:(SuccessBlock)successBlock {
if (goodsID == nil) {
NSException *exception = [[NSException alloc] initWithName:@"Order_viewControllerForMakeWithGoodsID:goodsCount:提示" reason:@"goodsID不能为空" userInfo:nil];
@throw exception;
}
if (goodsCount < 1) {
NSException *exception = [[NSException alloc] initWithName:@"Order_viewControllerForMakeWithGoodsID:goodsCount:提示" reason:@"goodsCount错误" userInfo:nil];
@throw exception;
}
NSMutableDictionary *params = [NSMutableDictionary dictionary];
params[@"goodsCount"] = [NSNumber numberWithInteger:goodsCount];
params[@"goodsID"] = goodsID;
if (successBlock) {
params[@"successBlock"] = successBlock;
}
UIViewController *viewController = [self performTarget:MediatorTargetOrder action:MediatorActionOrderMakeViewController params:params shouldCacheTarget:NO];
if ([viewController isKindOfClass:[UIViewController class]]) {
return viewController;
} else {
return [[UIViewController alloc] init];
}
}
@end
复制代码
订单模块的Target-Action实现中只须要在赋值操做时加入一行便可:
orderViewController.successBlock = params[@"successBlock"];
复制代码
商品详情页面的调用修改以下:
- (void)clickBuy {
__weak __typeof(self)weakSelf = self;
UIViewController *viewController = [[CCMediator sharedInstance] Order_viewControllerForMakeWithGoodsID:self.goodsID goodsCount:99 success:^(NSString * _Nonnull successString) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.textLabel.text = successString;
}];
[self.navigationController pushViewController:viewController animated:YES];
}
复制代码
详细的实现细节可去示例工程中查看。
通常的应用都是UITabBarController+UINavigationController,因此咱们的主工程基本都是搭建UITabBarController+UINavigationController的结构,作一些全局设置,以及处理一些初始化的逻辑等等。而后在Podfile里面引入全部的业务模块的Category工程以及对应的业务模块工程便可。
从模块间调用和通讯来看,解决依赖的办法也带来了一些硬编码的工做,包括调用时须要对类名和方法名进行硬编码,以及传递参数时对参数名的硬编码。这些硬编码没法避免,可是都在可控范围内,局限于Cateogry和对应的Target-Action。因此同一业务模块的Cateogry和Target-Action基本都是一我的编写,也能保证不会出错。
编写podspec文件时须要注意依赖循环的问题,须要注意:
这么作的缘由是,举个例子:好比帐户模块会调用商品模块的服务,商品模块也会调用帐户模块的服务。若是商品模块的Category工程的podspec依赖了商品模块的业务工程,同时帐户模块的Category工程的podspec依赖了帐户模块的业务工程。那么在商品模块的业务工程中引入帐户模块的Category工程时,就会引入帐户模块的业务工程。接着帐户模块就会引入商品模块的Category工程,商品模块的Category工程又引入了商品模块的业务工程中,而后就本身引入本身,因此确定没法引入成功。以下图所示:
tag小技巧,不少时候Git打完tag以后,在执行upload.sh上传pods的时候会出错。解决完错误后,会发现可能须要从新命名tag,致使版本号跳跃。因此能够删除失败的时候打的tag,而后从新打这个tag。
git tag -d 1.0.0
git push origin :/refs/tags/1.0.0
复制代码
至此,组件化方案实施的内容就到这里结束了。本文提供了基本的思路,已经能知足大部分的业务开发场景。可是对于经常使用的网络层、通用UI组件等这些部分没有涉及。这个时候就须要思考了,这些功能是属于业务类型的仍是非业务类型的。若是是业务类型的,那么最好是作成Category+Target-Action的方式对外提供服务;若是是非业务类型的,好比网络请求、通用UI框架等等,做者建议将这些功能封装到一个基础模块中,制做成组件后让全部业务模块引用便可。因为基础模块的组件是直接的文件引入,因此基础模块的功能不宜过多。由于一旦这个模块过于庞大,形成的依赖和耦合也会更大。同时随着项目的规模以及业务复杂度的提高,须要考虑的东西也会愈来愈多,这就更加考验系统架构的设计能力了。组件化的实践之路要一步一步走,也须要开发人员不断的思考、探索和完善。
以上都是做者在实际开发中的总结,交流请联系:
cctomato@outlook.com
本文全部的代码都托管在Github上,点击查看。
本文的架构设计和实践思路都来源于Casa Taloyum大神的博客: