何为组件化
一种能够解决代码耦合的技术。项目经过组件化的拆分,不仅可以解决代码耦合的问题,还可以增强代码的复用性,工程的易管理性,减少编译时间等
1.组件化分层架构图
2.架构分层详解
1.Lib层
基础模块跟业务无关,只定义接口和基本配置,子类可以重写,便于扩展
- LibBase
- LibBaseController
- LibBaseNavController
- ...
- LibCommon基础公共组件
- LibFlexBox移动端FlexBox布局
Widget层
- 由Lib延伸而来,便于组件的扩展和复用
2.Mediator层
- 采用Bifrost框架,创建调度组件并定义交互协议,处理业务模块之间的数据传递及逻辑交互处理
- Module层必须依赖该库
3.Module层
业务层,跟业务相关的一些组件,业务组件之间互不依赖,且依赖于ModuleCommon层
ModuleCommon
跟业务相关的公共组件部分,例如
- CommonBaseController
- CommonLodingController
- CommonListController
- 数据模型Models
- Tools工具类
- Macro常见宏等
- QMUI常见配置/换肤配置
- 第三方分享/登录/支付配置
3.组件化要点罗列
- 多Target分模块开发,代码解耦
- 单独编译项目
- 组件之间传值,通过调度组件Biforst
- 组件间访问公共图片资源,解决命名冲突
- 文件夹分层:子组件subspec
-
pod 引入依赖方式
- 引入本地依赖
- 引入远程依赖
- 引入指定分支
- 组件命名方式
- App路由管理
- podspec使用及格式校验
- 组件间依赖管理
- coapods私有化仓库搭建
**
3.1多Target分模块开发,代码解耦
workspace 依赖多个功能文件开发
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
def commonPods()
#基础宏定义,类别
pod 'HKMacros', :git => 'https://gitee.com/Steven_Hu/HKMacros.git'
# 组件化基类-引用本地依赖
pod 'HKBaseModule', :path => './PrivateRepo/HKBaseModule/HKBaseModule.podspec'
#pod 'HKBaseModule', :git => 'https://gitee.com/Steven_Hu/HKBaseModule.git'
end
def mediatorPods()
#pod 'Bifrost', :path => '../'
end
# Comment the next line if you don't want to use dynamic frameworks
#use_frameworks!
#workspace文件名
workspace 'HKiOSTools.xcworkspace'
#主工程路径
project 'HKiOSTools/HKiOSTools.xcodeproj'
target 'HKiOSTools' do
project 'HKiOSTools/HKiOSTools.xcodeproj'
commonPods()
target "HKBaseModule_Example" do
project 'PrivateRepo/HKBaseModule/Example/HKBaseModule.xcodeproj'
commonPods()
end
end
**
3.2单独编译项目
如何区分你编译的是主工程项目还是子工程项目
- 最简单的方式,通过项目名称:ProjectName
[[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleExecutableKey]
- 判断项目名称是否和子项目名称一致即可
3.3组件之间传值Bifrost
注册路由(ViewController)
+ (void)load {
[Bifrost bindURL:kRouteHomePage
toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
return [HKHomeViewController new];
}];
}
获取路由(ViewController)
UIViewController * vc = [Bifrost handleURL:kRouteHomePage];
[self.navigationController pushViewController:vc animated:YES];
页面传值
- 以type为例
//1.当前页面声明一个type属性
/// 验证类型
@property (nonatomic, assign) HKVerifyType type ;
// 2.bindURL
[Bifrost bindURL:kRouteBindPhoneNumPage toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
HKBindPhoneNumViewController *vc = [[self alloc] init];
vc.type = [parameters[kRouteBindPhoneNumParamType] integerValue];
return vc;
}];
// 3.传值type
NSString *routeURL = BFStr(@"%@?%@=%@", kRouteBindPhoneNumPage, kRouteBindPhoneNumParamType, @(HKVerifyTypeForgetPassword));
UIViewController * vc = [Bifrost handleURL:routeURL]
[self.navigationController pushViewController:vc animated:YES];
页面回调处理
//1.当前页面声明一个type属性和Block
/// 验证类型
@property (nonatomic, assign) HKVerifyType type ;
/// 完成回调
@property (nonatomic, strong) BifrostRouteCompletion complete ;
// 2.bindURL
[Bifrost bindURL:kRouteBindPhoneNumPage toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
HKBindPhoneNumViewController *vc = [[self alloc] init];
vc.type = [parameters[kRouteBindPhoneNumParamType] integerValue];
vc.complete = parameters[kBifrostRouteCompletion];
return vc;
}];
// 3.点击事件处理
/// 获取验证
- (void)getVerifyCodeEvent
{
[self.view endEditing:YES];
BFComplete(@{kBifrostRouteCompletion:self.complete}, @(self.type));
}
// 4.传值type
UIViewController *vc = [Bifrost handleURL:kRouteBindPhoneNumPage complexParams:@{kRouteBindPhoneNumParamType:@(HKVerifyTypeForgetPassword)} completion:^(id _Nullable result) {
HKVerifyType type = (YNVerifyType)result;
[HKKeyWindow hk_showWithText:HKStr(@"你点击的类型是:%@",type)];
}];
[self.navigationController pushViewController:vc animated:YES];
3.4组件间访问公共图片资源
主工程有一个
a.png
的图片,而pod
库里面也有一个a.png
的图片,此时就产生命名冲突了
思路:不同的组件都有自己独立的bundle,组件内部资源提供自身的Bundle来获取,以避免资源重复
- 本地资源如图片等存放位置(Assets文件夹,否则无法访问)
- 指定Bundle名称
# resource_bundles
s.resource_bundles = {
'HKLibCommon' => ['HKLibCommon/**/*.{xib,jpg,gif,png,xcassets}']
}
- 解决命名冲突:resource_bundles
-
resource_bundles
会自动生成bundle
把资源文件打包进去
加载本地资源方法
工具类抽取
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface ModuleBundle : NSObject
/*
* 根据bundle的名称获取bundle
*/
+ (NSBundle *)bundleWithName:(NSString *)bundleName;
//获取bundle 每次只要重写这个方法就可以在指定的bundle中获取对应资源
+ (NSBundle *)bundle;
//根据xib文件名称获取xib文件
+ (__kindof UIView *)viewWithXibFileName:(NSString *)fileName;
//根据图片名称获取图片
+ (UIImage *)imageNamed:(NSString *)imageName;
//根据sb文件名称获取对应sb文件
+ (UIStoryboard *)storyboardWithName:(NSString *)storyboardName;
//获取nib文件
+ (UINib *)nibWithName:(NSString *)nibName;
@end
#import "ModuleBundle.h"
@implementation ModuleBundle
+ (NSBundle *)bundleWithName:(NSString *)bundleName {
if(bundleName.length == 0) {
return nil;
}
NSString *path = [[NSBundle mainBundle] pathForResource:bundleName ofType:@"bundle"];
NSAssert([NSBundle bundleWithPath:path], @"not found bundle");
return [NSBundle bundleWithPath:path];
}
+ (NSBundle *)bundle {
// NSAssert([NSBundle mainBundle], @"not found bundle");
return [NSBundle mainBundle];
}
+ (UIView *)viewWithXibFileName:(NSString *)fileName {
NSAssert([self viewWithXibFileName:fileName inBundle:[self.class bundle]], @"not found view");
return [self viewWithXibFileName:fileName inBundle:[self.class bundle]];
}
+ (UIImage *)imageNamed:(NSString *)imageName {
NSAssert([self imageNamed:imageName inBundle:[self.class bundle]], @"not found image");
return [self imageNamed:imageName inBundle:[self.class bundle]];
}
+ (UIStoryboard *)storyboardWithName:(NSString *)storyboardName {
NSAssert([self storyboardWithName:storyboardName inBundle:[self.class bundle]], @"not found storyboard");
return [self storyboardWithName:storyboardName inBundle:[self.class bundle]];
}
+ (UINib *)nibWithName:(NSString *)nibName {
NSAssert([self nibWithNibName:nibName inBundle:[self.class bundle]], @"not found nib");
return [self nibWithNibName:nibName inBundle:[self.class bundle]];
}
#pragma mark - private
+ (UIImage *)imageNamed:(NSString *)imageName inBundle:(NSBundle *)bundle {
if(imageName.length == 0 || !bundle) {
return nil;
}
return [UIImage imageNamed:imageName inBundle:bundle compatibleWithTraitCollection:nil];
}
+ (UIImage *)imageNamed:(NSString *)imageName bundleName:(NSString *)bundleName {
return [self imageNamed:imageName inBundle:[self bundleWithName:bundleName]];
}
+ (UIView *)viewWithXibFileName:(NSString *)fileName inBundle:(NSBundle *)bundle {
if(fileName.length == 0 || !bundle) {
return nil;
}
//如果没有国际化,则直接去相应内容下的文件
UIView *xibView = [[bundle loadNibNamed:fileName owner:nil options:nil] lastObject];
if(!xibView) {
//文件国际化之后,所有的bundle的文件资源都在base的目录下
xibView = [[[NSBundle bundleWithPath:[bundle pathForResource:@"Base" ofType:@"lproj"]] loadNibNamed:fileName owner:nil options:nil] lastObject];
}
return xibView;
}
+ (UIView *)viewWithXibFileName:(NSString *)fileName bundleName:(NSString *)bundleName {
return [self viewWithXibFileName:fileName inBundle:[self bundleWithName:bundleName]];
}
+ (UIStoryboard *)storyboardWithName:(NSString *)storyboardName inBundle:(NSBundle *)bundle {
if(storyboardName.length == 0 || !bundle) {
return nil;
}
return [UIStoryboard storyboardWithName:storyboardName bundle:bundle];
}
+ (UIStoryboard *)storyboardWithName:(NSString *)storyboardName bundleName:(NSString *)bundleName {
return [self storyboardWithName:storyboardName inBundle:[self bundleWithName:bundleName]];
}
+ (UINib *)nibWithNibName:(NSString *)nibName inBundle:(NSBundle *)bundle {
if(nibName.length == 0 || !bundle ) {
return nil;
}
return [UINib nibWithNibName:nibName bundle:bundle];
}
@end
Bundle获取
创建一个类CommonBundle继承自ModuleBundle,并实现以下方法
#import "CommonBundle.h"
@implementation CommonBundle
+ (NSBundle *)bundle{
//TODO:注意Bundle名字需跟模块名称一致,否则会找不到path,直接Crash
return [self.class bundleWithName:@"HKLibCommon"];
}
@end
使用
替换系统加载图片的方式:[UIImage imageNamed:@"navigationbar_background"]
[CommonBundle imageNamed:@"navigationbar_background"]
多个包
spec.resource_bundles = {
'MapBox' => ['MapView/Map/Resources/*.png'],
'OtherResources' => ['MapView/Map/OtherResources/*.png']
}
3.5文件夹分层子组件subspec
我们在编写podspec文件时,sourcefiles只是告诉pods你需要哪些文件是这个项目中需要的,而没有包括文件的层级结构,那么就需要我们来实现这个层级结构:subspec
比如这里面的每一个文件夹,就是一个子pod,这样的好处是条理清晰,而且我们可以只用你需要的功能,在编写podfile时 就可以这样写
pod 'HKModuleModels/User'
只使用其中的一个功能。
主podspec
主pod可以是一个头文件,也可以具有一定的功能,我写的组件sourcefiles只是一个import子组件的头文件, sourcebundle是项目中需要的一些图片
编写subspec
- 让pods支持子subspec其实很简单,只要搞清楚三件事
- 文件夹结构 subspec sourcefiles的路径
- subspec 所依赖的系统库
- subspec 所依赖的第三方,和其它subspec的路径
3.6pod 引入依赖方式
引入本地依赖
pod 'ManageLocalCode', :path => '../ManageLocalCode'
pod 'BioAuthAPI', :path => '../BioAuthAPI'
pod 'HKTool', :path => '../'
远程私有库依赖:
# 基础宏定义byStevenHu
pod 'HKMacros', :git => 'https://gitee.com/Steven_Hu/HKMacros.git'
指定分支
# 支付代码测试
iap_pay:
git:
url: https://gitee.com/Steven_Hu/iap_pay.git
ref: master
常规依赖
pod "AFNetworking", "~>0.2.0"
3.7 组件命名方式
组件性质 | 建议名称 | 示例 |
---|---|---|
基础组件拆分 | 项目前缀Lib组件名称 | HKLibBase:基类模块 |
业务组件拆分 | 项目前缀Module组件名称 | HKModuleHome:首页模块 |
调度组件拆分 | 项目前缀Mediator组件名称 | HKMediatorDriver:司机端调度组件 |
Wdiget组件拆分 | 项目前缀Widget组件名称 | HKWidgetAddressPicker:地址选择器组件 |
公共组件 | 项目前缀_组件名称_Common | HKLibCommon,HKModuleCommon |
3.8 App端路由管理
格式
app内链
举例:yunquedriver://home/homepage/detail?name=steven&age=18
appscheme://moduleName/pageName/secondPageName?key1=value1&key2=value2
其中value中有中文等特殊字符时需要进行urlEncode。
app外链外链
格式说明
- appscheme分解
- 项目名称
- yunque
- 业务侧
- driver
- user
- 项目名称
- moduleName
- 业务模块名称
- pageName
- 页面名称
- secondPage
- 二级页面
scheme
云雀司机: yunquedriver
云雀用户:yunqueuser
模块名
lib:工具类,跟业务无关的
页面
3.9 podspec使用及格式校验
注意事项
- podspec版本号和git仓库代码的tag版本必须一致,否则找不到
- 比如:pod lib 默认生成的是0.1.0,我们根据业务需要改成0.0.1
- 索引仓库和组件仓库关联
- cd到组件仓库目录下
- pod repo push
#{本地关联到远程仓库的名字}
#{第三方私有库的podspec文件}
--verbose --allow-warnings 示例: pod repo push YNSpecs YNLibDemo.podspec --verbose --allow-warnings
- 项目引入
# 远程私有仓库
source 'https://gitlab.com/xxxx/xxSpecs'
# 引入使用
pod 'xxLibNetwork'
3.10 组件间依赖管理
通过 git submodule add https://gitee.com/Steven_Hu/hk-module-components.git 添加submodule
会在git上添加一个.gitmodules 文件,可以查看各种依赖
[submodule "PrivateRepo/hk-lib-base"]
path = PrivateRepo/hk-lib-base
url = https://gitee.com/Steven_Hu/hk-lib-base.git
[submodule "PrivateRepo/hk-lib-common"]
path = PrivateRepo/hk-lib-common
url = https://gitee.com/Steven_Hu/hk-lib-common.git
[submodule "PrivateRepo/hk-lib-network"]
path = PrivateRepo/hk-lib-network
url = https://gitee.com/Steven_Hu/hk-lib-network.git
[submodule "PrivateRepo/hk-mediator"]
path = PrivateRepo/hk-mediator
url = https://gitee.com/Steven_Hu/hk-mediator.git
[submodule "PrivateRepo/hk-lib-keyboard"]
path = PrivateRepo/hk-lib-keyboard
url = https://gitee.com/Steven_Hu/hk-lib-keyboard.git
[submodule "PrivateRepo/hk-lib-avoid-app-crash"]
path = PrivateRepo/hk-lib-avoid-app-crash
url = https://gitee.com/Steven_Hu/hk-lib-avoid-app-crash.git
[submodule "PrivateRepo/hk-lib-load-picture"]
path = PrivateRepo/hk-lib-load-picture
url = https://gitee.com/Steven_Hu/hk-lib-load-picture.git
[submodule "PrivateRepo/hkmodule-models"]
path = PrivateRepo/hkmodule-models
url = https://gitee.com/Steven_Hu/hkmodule-models.git
[submodule "PrivateRepo/hk-module-uikit"]
path = PrivateRepo/hk-module-uikit
url = https://gitee.com/Steven_Hu/hk-module-uikit.git
[submodule "PrivateRepo/hk-module-components"]
path = PrivateRepo/hk-module-components
url = https://gitee.com/Steven_Hu/hk-module-components.git
[submodule "PrivateRepo/hk-module-main"]
path = PrivateRepo/hk-module-main
url = https://gitee.com/Steven_Hu/hk-module-main.git
最终结果如下图所示,点击跳转新的submodule仓库地址
本地Clone方式
git clone https://gitee.com/Steven_Hu/hk-iostools.git
git submodule init && git submodule update
#下面这一句的效果和上面三条命令的效果是一样的,多加了个参数 `--recursive`
git clone https://gitee.com/Steven_Hu/hk-iostools.git --recursive
常见问题
error: Server does not allow request for unadvertised object b22e0a5a2a8dce1d0454bdead70353bf45f33f81
Fetched in submodule path 'PrivateRepo/hk-module-main', but it did not contain b22e0a5a2a8dce1d0454bdead70353bf45f33f81. Direct fetching of that commit failed.
原因分析:
未拉取到该submodule的最新代码
解决
git submodule foreach git checkout master
git submodule foreach git submodule update
3.11 coapods私有化仓库搭建
准备工作
- 安装cocoapods
- 准备gitlab/gitee/getlab(下文统一称为gitlab)等代码仓库账号
创建组建索引Spec仓库(用于存放组件库的索引)
- 在gitlab中创建
XXSpecs
仓库,初始化README.md(目的是仓库不为空)得到仓库地址 - 在本地cocoapods中添加repo
pod repo add `specFileName(给spec仓库在本地的命名)` `spec(仓库的地址)`
实例:
pod repo add XXSpecs https://gitee.com/Steven_Hu/objective-c-specs
在~/.cocoapods/repos
目录中可以看到创建的文件夹
- 创建组件库
- pod lib create #{库的名称}
- 实例 pod lib create XXLibDemo
- 修改podspec文件
- 将组件push到gitlab
- 将索引仓库和组件仓库关联
- 项目引入
# 远程私有仓库
source 'https://gitlab.com/xxxx/xxSpecs'
# 引入使用
pod 'xxLibNetwork'
4.常见问题补充
4.1target has transitive dependencies that include statically linked binaries:
解决办法
s.static_framework = true
4.2CocoaPods did not set the base configuration of your project because your project already...
使用CocoaPods安装三方库后有两个警告,如下所示:
解决办法:
将第三方库 的 PROJECT → Info → Configurations 下Debug和Release下的.debug和.release选项替换为None,如下图所示:
然后在pod install
即可
4.3清除 CocoaPods 本地缓存
特殊情况下,由于网络或者别的原因,通过CocoaPods下载的文件可能会有问题。
这时候您可以删除CocoaPods的缓存(~/Library/Caches/CocoaPods/Pods/Release目录),再次导入即可。
4.4pod spec lint编译时报error: include of non-modular header inside framework module
解决办法
pod lib lint --allow-warnings --use-libraries --verbose
有警告⚠️可以使用-allow-warnings忽略。
4.5推送到本地LocalRepo
- 使用了第三方库
pod repo push driver_spec XXLibBase.podspec --allow-warnings --verbose --use-libraries
- 未使用第三方库
pod repo push driver_spec XXLibBase.podspec --allow-warnings --verbose
4.6pod lib lint 可选参数
pod lib lint SPEC_NAME.podspec
可选参数:
--verbose : 显示详细信息
--allow-warnings: 是否允许警告,用到第三方框架时,用这个参数可以屏蔽讲稿
--fail-fast: 在出现第一个错误时就停止
--use-libraries:如果用到的第三方库需要使用库文件的话,会用到这个参数
--sources:如果一个库的podspec包含除了cocoapods仓库以外的其他库的引用,则需要改参数指明,用逗号分隔。
--subspec=Name:用来校验某个子模块的情况。
注意:如果库用到了第三方的话要带上 --use-libraries,否则会报错,上传不上去。
4.7--sources 使用
1.私有pod的验证
使用pod spec lint去验证私有库能否通过验证时应该,应该要添加--sources选项,不然会出现找不到repo的错误。
pod spec lint --sources='私有仓库repo地址,https://github.com/CocoaPods/Specs'
2.私有库引用私有库的问题
在私有库引用了私有库的情况下,在验证和推送私有库的情况下都要加上所有的资源地址,不然pod会默认从官方repo查询。
pod spec lint --sources='私有仓库repo地址,https://github.com/CocoaPods/Specs'
pod repo push 本地repo名 podspec名 --sources='私有仓库repo地址,https://github.com/CocoaPods/Specs'
4.组件化案例
1.效果图
2.代码地址
https://gitee.com/Steven_Hu/hk-iostools
搬砖不易,转载请注明出处,谢谢!