为什么需要组件化
- 模块间解耦
- 模块重⽤
- 提⾼团队协作开发效率
- 单元测试
什么情况不需要组件化
- 项⽬较⼩,模块间交互简单,耦合少
- 模块没有被多个外部模块引⽤,只是⼀个单独的⼩模块
- 模块不需要重⽤,代码也很少被修改
- 团队规模很⼩
- 只能上层对下层依赖
- 项⽬公共代码资源 下沉
- 横向的依赖 最好下沉
CocoaPods
组件开发完成,会上传Spec到这里,包含着所有的版本信息,然后通过这么文件找到地址信息。我们在本地都有个隐藏文件,可以看到本地的Specs,例如afn的Spec信息/Users/xxx/.cocoapods/repos/trunk/Specs/a/7/5/AFNetworking


组件化分层

组件化之前我们需要继续解耦,模块分级.
首先创建基础模块,在项目目录下,终端
pod lib create JMacroAndCategoryModule
JMacroAndCategoryModule
是你的模块名称
模板信息.png
通过这个我们可以创建一个基础模板https://github.com/CocoaPods/pod-template.git
在本地。
生成的信息在 JMacroAndCategoryModule.podspec
中
#
# Be sure to run `pod lib lint JMacroAndCategoryModule.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'JMacroAndCategoryModule'
#发版版本号,每更新一次代码就改变一次版本号
s.version = '0.1.0'
#一个简单的总结,随便写
s.summary = 'A short description of JMacroAndCategoryModule.'
# This description is used to generate tags and improve search results.
# * Think: What does it do? Why did you write it? What is the focus?
# * Try to keep it short, snappy and to the point.
# * Write the description between the DESC delimiters below.
# * Finally, don't worry about the indent, CocoaPods strips it!
#描述,随便写 但是要比 s.summary 长度长
s.description = <<-DESC
TODO: Add long description of the pod here.
DESC
#你的 git 仓库首页的网页 url,注意并不是 https/ssh这种代码仓库地址
s.homepage = 'https://github.com/jscmkt/JMacroAndCategoryModule'
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
#直接写 MIT
s.license = { :type => 'MIT', :file => 'LICENSE' }
#你是谁
s.author = { 'jscmkt' => 'xxx@qq.com' }
#这里就是你 git 仓库的 https/ssh 地址了
s.source = { :git => 'https://github.com/jscmkt/JMacroAndCategoryModule.git', :tag => s.version.to_s }
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
s.ios.deployment_target = '8.0'
#这里的文件夹下的内容就是这个 pods 被pod install 的时候会被下载下来的文件,不在这个文件夹,将不会被引用
# Classes 目录和.podspec 目录是平级的。
#你可以随便指定文件夹名称,只要这个文件夹是真实存在的
#Classes/**/*.{h,m},表示 Classes 文件夹及其文件夹下的所有.h,.m 文件。
s.source_files = 'JMacroAndCategoryModule/Classes/**/*'
#资源文件地址,下面的所有.png资源都被打包成 s.name.bundle
s.resource = ['Images/*.png','Sounds/*']
#资源文件地址,和 resource 的区别是,这个属性可以指定 bundle 的名字,下面的所有.png文件都会被打包成 ABC_section.bundle
# s.resource_bundles = {
# 'JMacroAndCategoryModule' => ['JMacroAndCategoryModule/Assets/*.png']
# }
#指定公有头文件,如果没有写,那么所有 pod 中的头文件都默认公有,可以被 import。如果指定了某些头文件,那么只有这些被指定的头文件才可以被 import。
# s.public_header_files = 'Pod/Classes/**/*.h'
# s.frameworks = 'UIKit', 'MapKit'
#这个 pods 还依赖于其他哪些 pods
# s.dependency 'AFNetworking', '~> 2.3'
end
我们可以根据官网信息来进行配置

头文件处理
我们把基础模块的代码放在Classes位置
进入Example终端执行pod install
,更新依赖。
我们在通过pod lib create JCommonUIModule
创建一个JCommonUIModule
.
build一下,发现会有三方库的报错,我们在JCommonUIModule.podspec
添加
s.dependency 'AFNetworking'
s.dependency 'Masonry'
s.prefix_header_contents = '#import "Masonry.h"','#import "UIKit+AFNetworking.h"''
这里我们就解决了三方依赖。现在我们需要解决模块间的依赖。
我们需要给他一个查询路径,在JCommonUIModule
的Podfile
中添加
pod 'JMacroAndCategoryModule', :path => '../../JMacroAndCategoryModule'
资源文件处理
我们通过self.imageView.image = [UIImage imageNamed:@"share_wechat"
发现无法加载资源文件。

首先我们需要在将资源文件放在Assets
文件夹中,然后在podspec
中添加资源目录。
s.resource_bundles = {
'LGModuleTest' => ['LGModuleTest/Assets/*']
}
执行pod install
在项目中通过资源包加载。
NSString *bundlePath = [[NSBundle bundleForClass:[self class]].resourcePath stringByAppendingPathComponent:@"/JModuleTest.bundle"];
NSBundle *resoure_bundle = [NSBundle bundleWithPath:bundlePath];
self.imageView.image = [UIImage imageNamed:@"share_wechat" inBundle:resoure_bundle compatibleWithTraitCollection:nil];
xib和json也是这样处理
*****************xib********************
NSString *bundlePath = [NSBundle bundleForClass:[self class]].resourcePath;
[self.tableView registerNib:[UINib nibWithNibName:className bundle:[NSBundle bundleWithPath:bundlePath]] forCellReuseIdentifier:className];
*****************json********************
NSString *bundlePath = [[NSBundle bundleForClass:[self class]].resourcePath stringByAppendingPathComponent:@"/JHomeModule.bundle"];
NSString *path = [[NSBundle bundleWithPath:bundlePath] pathForResource:[NSString stringWithFormat:@"Home_TableView_Response_%@", channelId] ofType:@"json"];
中间层依赖
为了解决模块和模块间通讯,我们使用Mediator。
VC1 想PUSH VC2 需要有响应+参数+回调
VC2需要有响应方法
通过runtime我们知道方法都有消息接收者和消息主体。
这个时候就需要暴露接口:target-action,VC2需要来维护他。
这个时候我们就需要Mediator
。Mediator
来帮助找target-action。
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
if (targetName == nil || actionName == nil) {
return nil;
}
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// generate target
NSString *targetClassString = nil;
if (swiftModuleName.length > 0) {
targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
NSObject *target = [self safeFetchCachedTarget:targetClassString];
if (target == nil) {
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}
// generate action
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
if (target == nil) {
// 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
if (shouldCacheTarget) {
[self safeSetCachedTarget:target key:targetClassString];
}
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 这里是处理无响应请求的地方,如果无响应,则尝试调用对应target的notFound方法统一处理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程中,可以用前面提到的固定的target顶上的。
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
@synchronized (self) {
[self.cachedTarget removeObjectForKey:targetClassString];
}
return nil;
}
}
}
这就是Mediator
主要方法。
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
if(methodSig == nil) {
return nil;
}
const char* retType = [methodSig methodReturnType];
if (strcmp(retType, @encode(void)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
return nil;
}
.......
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}
简单的返回值类型直接处理,其他的交给target performSelector
处理.
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
{
// 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}
最后直接返回需要跳转的页面。
使用方法
//需要调用的地方
UIViewController *vc = [[CTMediator sharedInstance] CTMediator_naviagetPlayerVC:((HomeTemplateData *)data).videoId title:((HomeTemplateData *)data).title];
[[UIViewController currentViewController].navigationController pushViewController:vc animated:YES];
新建CTMediator分类
///处理跳转方法
#import "CTMediator+LGPlayerModuleAction.h"
NSString * const kCTMediatorTargetLGPlayerModule = @"LGPlayerModule";
NSString * const kCTMediatorActionNativeLGPlayerViewController = @"nativeLGPlayerViewController";
@implementation CTMediator (LGPlayerModuleAction)
- (UIViewController *)CTMediator_naviagetPlayerVC:(NSString *)videoID title:(NSString *)videoTitle{
UIViewController *viewController = [self performTarget:kCTMediatorTargetLGPlayerModule
action:kCTMediatorActionNativeLGPlayerViewController
params:@{@"videoID":videoID,
@"videoTitle":videoTitle
}
shouldCacheTarget:NO
];
return viewController;
}
@end
整合项目
新建个工程,导入写好的模块
pod 'JHomeModuleCategory', :path => '../JHomeModuleCategory'
pod 'JMacroAndCategoryModule', :path => '../JMacroAndCategoryModule'
pod 'JCommonUIModule', :path => '../JCommonUIModule'
pod 'JPlayerModuleCategory', :path => '../JPlayerModuleCategory'
pod 'JHomeModule', :path => '../JHomeModule'
pod 'JPlayerModule', :path => '../JPlayerModule'
pod 'JDLNAModule', :path => '../JDLNAModule'
调试一下就可以了。
网友评论