组件化初探
为什么需要组件化
- 模块间解耦
- 模块重用
- 提高团队协作开发效率
- 便于单元测试
哪些项目不需要组件化
- 项目较小,模块之间交互简单、耦合少
- 模块没有被多个外部模块引用,只是一个简单的小模块
- 模块不需要重用,代码也很少被修改
- 团队规模小
组件化分层设计
底层组件通常包括底层类库、分类、宏定义文件等
注意:组件化分层实际就是通过接口隔离实现分层之间的依赖关系,而且只能上层对下层依赖项⽬公共代码资源下沉,横向的依赖也最好下沉
cocoapods 创建私有库
私有库创建流程如下:
写个 Demo 说明一下具体流程
1、在终端上移动到你想创建pod库工程的路径,并执行pod库的创建。命令如下:
plz@plzdeMacBook-Pro:~ better$ cd desktop
plz@plzdeMacBook-Pro:desktop better$ pod lib create PrivateHelloWorld
回车之后,终端会询问几个问题
# 选择编程语言
What language do you want to use?? [ Swift / ObjC ]
> Objc
# 在你的项目中是否创建一个demo工程,为了方便测试,我选择了Yes
Would you like to include a demo application with your library? [ Yes / No ]
> Yes
# 测试框架选择哪一个
Which testing frameworks will you use? [ Specta / Kiwi / None ]
> None
#要不要做视图测试
Would you like to do view based testing? [ Yes / No ]
> NO
# 类前缀名
What is your class prefix?
> HG
Pod私有库创建成功。一般来说创建成功会自动打开项目
2、安装CocoaPods项目找到刚才创建的PrivateHelloWorld文件夹,点击入去,里面的目录如下
3、添加你要添加代码文件(复制粘贴)
我这里放了PrintHelloWorld文件 pod install
一下
# 移到Example目录下
BetterdeMacBook-Pro:desktop better$ cd /Users/better/Desktop/PrivateHelloWorld/Example
# 安装CocoaPods项目
BetterdeMacBook-Pro:Example better$ pod install --no-repo-update
先进去Example文件夹点击后缀为xcworkspace的文件打开项目,运行下是否成功。
4、编辑CocoaPods的配置文件(后缀名为podspec)
打开PrintHelloWorld文件夹就可以看到
可以有很多种编辑方式编辑这个文件,如Xcode、文本编辑器、Sublime Text系列、Atom
我是用文本编辑器打开,难看是难看点,但胜在够方便
s.version我习惯是0.0.1开始
s.summary需要改改,不然待会提交会报错
s.homepage这里随便写个网站都行,建议写项目的首页,但一定要改,不然默认的会报错,因为没有默认的网址
s.source需要填一个git地址的私有库,github收费的,我选择的gitLab
复制私有库地址,在.podspec文件内的s.source替换地址
配置完成了
再次移到我们的Example文件,pod更新一下
BetterdeMacBook-Pro:Example better$ pod update --no-repo-update
打开项目,看看是否成功了
5、添加PrintHelloWorld,运行测试打开项目,在BYViewController.m里面导入PrintHelloWorld.h文件
6、验证pod配置文件为了保证项目正确性,pod文件配置没问题,在提交之前,我们需要验证一下
用终端移到我们的项目路径
BetterdeMacBook-Pro:~ better$ cd /Users/better/Desktop/PrivateHelloWorld
到这里,我们已经完成源码导入、验证项目是否能运行、pod配置文件本地验证了
7、项目发布,tag 0.0.1终端移到该项目文件下执行git的相关命令
# 添加远程地址,即上面创建码云项目的地址
BetterdeMacBook-Pro:PrivateHelloWorld better$ git remote add origin https://gitee.com/Better_Y/PrintHelloWorld.git
# 添加文件
BetterdeMacBook-Pro:PrivateHelloWorld better$ git add .
# 提交本地,并写描述
BetterdeMacBook-Pro:PrivateHelloWorld better$ git commit -a -m "第一次提交 版本为0.0.1"
# --allow-unrelated-histories
# git pull origin maste会失败 ,提示:fatal: refusing to merge unrelated histories
# 原因是远程仓库origin上的分支master和本地分支master被Git认为是不同的仓库,所以不能直接合并,需要添加 --allow-unrelated-histories
BetterdeMacBook-Pro:PrivateHelloWorld better$ git pull origin master --allow-unrelated-histories
# 推送到码云的PrintHelloWolrd项目的master分支上
BetterdeMacBook-Pro:PrivateHelloWorld better$ git push origin master
# 提交版本号
BetterdeMacBook-Pro:PrivateHelloWorld better$ git tag 0.0.1
# push到远程分支
BetterdeMacBook-Pro:PrivateHelloWorld better$ git push origin 0.0.1
8、创建Sepc管理库 创建步骤跟上面码云创建的git私有库同理
在终端执行Specs创建命令
BetterdeMacBook-Pro:PrivateHelloWorld better$ pod repo add PrintSpecs https://gitlab.com/peiliuzhen/testspecs.git
现在,我们可以直接发布了
# PrintSpecs是刚才上面添加的管理库名字
# PrivateHelloWorld.podspec是PrintHelloWorld项目里面后缀为podspec的文件名
BetterdeMacBook-Pro:PrivateHelloWorld better$ pod repo push PrintSpecs PrivateHelloWorld.podspec
发布成功后,我们可以去码云看看PrivateSpecs的git项目有没有提交成功
到这里,我们的私有库发布已经全部完成了
9、检验私有库发布
新建privateDemo项目,创建Podfile文件并安装
Podfile代码如下
platform :ios,'8.0'
target 'privateDemo' do
pod 'PrintHelloWorld',:git => 'https://gitlab.com/peiliuzhen/privatehelloworld.git'
end
打开终端并执行pod安装指令:
BetterdeMacBook-Pro:~ better$ cd /Users/better/Desktop/privateDemo
BetterdeMacBook-Pro:privateDemo better$ pod install --no-repo-update
我们打开PrivateDemo项目目录看看
到这里,验证我们的私有库发布就完满结束了!!
组件化基本操作
抽取解封装
使用一个常见的列表页举例说明:
cell的封装抽离:
组件化模块处理
模块耦合
模块解耦
组件通信
模块之间通信
开源组件化框架推荐(MJRouter、CTMediator、BeeHive)
目前主流的组件化方式有三种
-
URL
路由 -
target-action
-
protocol
匹配
下面介绍一下三种方式对应的代表性框架
MJRouter(URL路由)
iOS常用路由工具一般都是使用URL匹配,或者根据约定的命名使用runtime方法进行动态调用。
这种实现方式优点是实现简单,缺点是需要维护字符串表,或者根据依赖命名约定无法在编译时暴露问题,运行时才发现错误。
URL 路由方式典型代表框架是蘑菇街开源的 MGJRouter,还有 JLRoutes,HHRouter 等
MGJRouter 实现分析:
- App 启动时实例化各组件模块或者使用class注册,然后组件向
ModuleManager
注册Url
- 当组件A需要调用组件B时,向
ModuleManager
传递URL,参数可以拼接在URL后面或者放在字典里传递,类似openURL
。然后由ModuleManager
负责调度组件B,最后完成目标
注册及调用代码如下:
// 1、注册某个URL[MGJRouter registerURLPattern:@"mgj://foo/bar" toHandler:^(NSDictionary *routerParameters) { NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]); }]; //2、调用路由[MGJRouter openURL:@"mgj://foo/bar"]; [MGJRouter registerURLPattern:@"mgj://category/travel" toHandler:^(NSDictionary *routerParameters) { NSLog(@"routerParameters[MGJRouterParameterUserInfo]:%@", routerParameters[MGJRouterParameterUserInfo]); // @{@"user_id": @1900} }]; [MGJRouter openURL:@"mgj://category/travel" withUserInfo:@{@"user_id": @1900} completion:nil];
URL 路由的优点
-
具有很高的动态性,适合经常开展运营活动的app,例如蘑菇街自身属于电商行业
-
方便统一管理多平台的路由规则
-
易于适配 URL Scheme
URl 路由的缺点
-
传参方式有限,并且无法利用编译器进行参数类型检查,因此所有的参数都是通过字符串转换而来
-
只适用于界面模块,不适用于通用模块
-
参数的格式不明确,是个灵活的 dictionary,也需要有个地方可以查参数格式。
-
不支持storyboard
-
依赖于字符串硬编码,难以管理,蘑菇街做了个后台专门管理。
-
无法保证所使用的的模块一定存在
-
解耦能力有限,url 的”注册”、”实现”、”使用”必须用相同的字符规则,一旦任何一方做出修改都会导致其他方的代码失效,并且重构难度大
CTMediator(target-action)
target-action 方案是基于OC的runtime、category特性动态获取模块,例如通过NSClassFromString
获取类并创建实例,通过performSelector + NSInvocation
动态调用方法,典型代表框架是 CTMediator
实现分析:
-
利用分类为路由添加新接口,在接口中通过字符串获取对应的类
-
通过runtime创建实例,动态调用实例的方法
页面间跳转实现代码如下:
//******* 1、分类定义新接口
#import "CTMediator+A.h"
@implementation CTMediator (A)
- (UIViewController *)A_Category_Swift_ViewControllerWithCallback:(void (^)(NSString *))callback
{
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
params[@"callback"] = callback;
params[kCTMediatorParamsKeySwiftTargetModuleName] = @"A_swift";
return [self performTarget:@"A" action:@"Category_ViewController" params:params shouldCacheTarget:NO];
}
- (UIViewController *)A_Category_Objc_ViewControllerWithCallback:(void (^)(NSString *))callback
{
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
params[@"callback"] = callback;
return [self performTarget:@"A" action:@"Category_ViewController" params:params shouldCacheTarget:NO];
}
@end
//******* 2、模块提供者提供target-action的调用方式(对外需要加上public关键字)
#import "Target_A.h"
#import "AViewController.h"
@implementation Target_A
- (UIViewController *)Action_Category_ViewController:(NSDictionary *)params
{
typedef void (^CallbackType)(NSString *);
CallbackType callback = params[@"callback"];
if (callback) {
callback(@"success");
}
AViewController *viewController = [[AViewController alloc] init];
return viewController;
}
- (UIViewController *)Action_Extension_ViewController:(NSDictionary *)params
{
typedef void (^CallbackType)(NSString *);
CallbackType callback = params[@"callback"];
if (callback) {
callback(@"success");
}
AViewController *viewController = [[AViewController alloc] init];
return viewController;
}
//******* 3、使用
// Objective-C -> Category -> Objective-C
UIViewController *viewController = [[CTMediator sharedInstance] A_Category_Objc_ViewControllerWithCallback:^(NSString *result) {
NSLog(@"%@", result);
}];
[self.navigationController pushViewController:viewController animated:YES];
优点
-
利用
分类
可以明确声明接口,进行编译检查 -
实现方式
轻量
缺点
-
需要在
mediator
和target
中重新添加每一个接口,模块化时代码较为繁琐 -
在
category
中仍然引入了字符串硬编码
,内部使用字典传参 -
无法保证使用的模块一定存在,target在修改后,使用者只能在运行时才能发现错误
-
可能会创建过多的 target 类
CTMediator 核心源码分析
通过分类中调用的
performTarget
来到CTMediator
中的具体实现,即performTarget:action:params:shouldCacheTarget:
,主要是通过传入的name,找到对应的target
和action
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
if (targetName == nil || actionName == nil) {
return nil;
}
//在swift中使用时,需要传入对应项目的target名称,否则会找不到视图控制器
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// generate target 生成target
NSString *targetClassString = nil;
if (swiftModuleName.length > 0) {
//swift中target文件名拼接
targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
//OC中target文件名拼接
targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
//缓存中查找target
NSObject *target = [self safeFetchCachedTarget:targetClassString];
//缓存中没有target
if (target == nil) {
//通过字符串获取对应的类
Class targetClass = NSClassFromString(targetClassString);
//创建实例
target = [[targetClass alloc] init];
}
// generate action 生成action方法名称
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
//通过方法名字符串获取对应的sel
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];
}
//是否响应sel
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;
}
}
}
进入
safePerformAction:target:params:
实现,主要是通过invocation
进行参数传递+消息转发
- (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];
//void类型
if (strcmp(retType, @encode(void)) == 0) {
...
}
//...其他类型可以阅读CTMediator源码
}
BeeHive(protocol class)
protocol匹配的实现思路
是:
-
1、将
protocol
和对应的类
进行字典匹配
-
2、通过用
protocol
获取class
,在动态创建实例
protocol比较典型的三方框架就是 阿里的BeeHive。BeeHive
借鉴了Spring Service、Apache DSO的架构理念,采用AOP+扩展App生命周期API
形式,将业务功能
、基础功能
模块以模块方式以解决大型应用中的复杂问题,并让模块之间以Service形式调用
,将复杂问题切分,以AOP方式模块化服务。
BeeHive 源码还没有深入探究,想了解的可以参考链接: BeeHive — 一次iOS模块化解耦实践
附参考文章:
今天的文章iOS 组件化方案总结分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/21620.html