组件化的意义
组件化就是将一个项目,拆分为不同的组件:
服务组件:通常为工具通用类代码,如网络请求库,路由router,界面布局库,加密解密库等等...
数据组件:通常为数据通用类代码,如获取本地资源文件,APP初始化配置文件,本地存储信息等等...
业务组件:通常为不通用代码,主要是涉及到app的模块之间的组件
组件化的好处顾名思义,解耦合,便于团队开发,也便于开发人员进行单元测试。
下图是以联影智学为例的组件化方案架构设计
实现方案
步骤:
-
Router:采取Target Action方式实现
-
通过 Cocoapods 搭建私有库,创建相应的模版
-
实现各个模块工程,处理相互依赖配置项
-
二进制文件(组件编译为静态包)存储到公司服务器上
-
(下一步计划: )系统管理 pod 库,后续可通过一系列脚本实现自动化(Cocoapods 配置说明查询,组件版本依赖,统一集成等)。
下面我们一一介绍每个步骤的实现
-
Router:采取Target Action方式实现
UIHRouter通过Runtime的动态创建类的NSClassFromString动态去获取对应的类,通过NSSelector动态的获取该类的方法,将传入的参数通过- (id)performSelector:(SEL)aSelectorwithObject:(id)object进行调用。动态加载路由信息.
-
组件对外公开接口
每个组件都有一个module类存放路由方法的定义以及实现,这其中主要是由四个参数组成
-
路由的地址:协议名称+组件名称组件方法
-
路由的参数:组件需要用到的参数
-
路由的报错信息:调用方错误,或者是自身组件错误等
-
路由的返回信息:一般在返回值或者在callBack闭包参数中获取
当团队间成员进行app的联调时,会知道彼此模块的模块名称,以及模块的方法,这里就类似于url 协议://模块名称/方法名称/参数,通过知道对方的协议,然后进行组件之间的通信,可能是获取一个视图控制器对象,可能是获取一堆请求的数据,可能是获取一个视图组件。
定义一个类方法可以让 Router 通过 URI 里的内容可以映射过去,不需要协议注册和协议管理,我们使用宏替换,在 Router 里提供一个输出的规范,如下:
// 组件对外公开接口 m: 组件名, i: 接口名, p(arg): 接收参数, c(callback): 回调block
#define UIHROUTER_EXTERN_METHOD(m,i,p,c) + (id) routerHandle_##m##_##i:(NSDictionary*)arg callback:(id)callback
-
声明并实现协议方法
通过Router调用,声明一个协议方法,完成 a 到 b 的通信:
+(void)openURL:(NSString *)stringUrl arg:(NSDictionary *)param error:(NSError *)error callBack:(void (^)(void))callBack;
在这里,openaUrl的实现功能, 通过传过来的stringUrl和param参数,根据对应规则解析出公开接口名字,对应组件名字以及通信参数, objc_msgSend函数实现运行时调用.具体实现方式可以参照demo中的UIH_RouterModule组件的UIHRouter类.
+(void)openURL:(NSString *)stringUrl arg:(NSDictionary *)param error:(NSError *)error callBack:(void (^)(void))callBack
{
if (![stringUrl containsString:@"router://"]) {
NSLog(@"illegal router");
return;
}
NSMutableDictionary *params = [NSMutableDictionary dictionaryWithDictionary:param];
NSString *pathStr = [[stringUrl componentsSeparatedByString:@"//"] lastObject];
if ([stringUrl containsString:@"?"]) {
NSArray *paramsArrary = [stringUrl componentsSeparatedByString:@"?"];
NSString *paramsStr = [paramsArrary lastObject];
for (NSString *paramStr in [paramsStr componentsSeparatedByString:@"&"]) {
NSArray *elts = [paramStr componentsSeparatedByString:@"="];
if([elts count] < 2) continue;
[params setObject:[elts lastObject] forKey:[elts firstObject]];
}
NSArray *pathStrArrary = [pathStr componentsSeparatedByString:@"?"];
pathStr = [pathStrArrary firstObject];
}
NSArray *pathElementsArrary = [pathStr componentsSeparatedByString:@"/"];
NSString *moduleStr = [pathElementsArrary firstObject];
NSString *actionStr = [pathElementsArrary lastObject];
NSString *routerStr = [NSString stringWithFormat:@"routerHandle_%@_%@:callback:",moduleStr,actionStr];
Class targetClass = NSClassFromString(moduleStr);
SEL action = NSSelectorFromString(routerStr);
((void(*)(id,SEL,id,id))objc_msgSend)(targetClass, action,params,callBack);
if (callBack) {
callBack();
}
}
objc_msgSend用法
在这里做一下简单介绍:
void objc_msgSend(id self, SEL cmd, ... );
方法的调用本质是 objc_msgSend来发送消息,以下是其函数原型:
这是个参数个数可变的函数,能接受两个或两个以上的参数。第一个参数代表组件名接受者,第二个参数代表所要调用的方法的名字,后续参数就是消息中的那些参数,其顺序不变。
objc_msgSend函数会依据组件名称与方法名的类型来找到整个项目中适当的方法进行调用。
-
实现组件入口
在各个组件中, 为了输出规范统一,在组件入口会将步骤1.1声明的类方法变为宏替换输出
通过步骤1.2中objc_msgSend方法,会找到对应组件的一下方法进行调用
// UIHROUTER_EXTERN_METHOD: 组件对外公开接口
// UIHIOSCommon: m组件名,
// common1: 接口名,
// arg: 接收参数,
// callback: 回调block
UIHROUTER_EXTERN_METHOD(UIHIOSCommon, common1, arg, callback) {
NSLog(@"TestModule get arg=%@",arg);
return nil;
}
UIHROUTER_EXTERN_METHOD(UIHIOSCommon, common2, arg, callback) {
NSLog(@"TestModule get arg=%@",arg);
return nil;
}
-
调用组件
这样我们就有了进入组件的入口,调用方式如下(两种方式等效):
UIHRouter.openURL("router://UIHIOSCommon/common1?from=test&todo=goto", arg: nil, error: nil, callBack: nil)
UIHRouter.openURL("router://UIHIOSCommon/common2", arg: ["from":"test", "todo":"goto", ], error: nil, callBack: nil)
-
搭建组件库
-
Pod方式创建组件工程
在项目同目录下,打开终端使用pod命令创建组件工程,取名为UIH_RouterModule
pod lib create WJCommon
按回车就会自动生成组件工程,看一下效果如下:
-
目录介绍
可以看到对比上面Xcode打开的工程中的路径,其实是有所出入的,这样是为了更方便我们测试代码与工程代码的解耦.
-
Example是组件的测试用例,可以用于测试UIHIOSCommon组件里的API。可以写一些UIView相关类去测试组件代码的正确,
-
Example下面的Podfile文件中,添加该测试用例所依赖的三方库
-
UIHIOSCommon是真正的组件的代码相关
-
UIHIOSCommon.podspec是用来配置组件的版本号、名称、描述、作者信息、远程仓库链接、依赖三方库、开放资源文件等等。在初始化的时候pod已经根据你的git账号自动生成相关内容,你也可以进行手动修改.
podspec文件中中添加依赖的三方库,是s.dependency, pod install之后就会在Pods文件夹下.
s.dependency 'RxSwift'
s.dependency 'Masonry'
s.dependency 'UIH_RouterModule'
在集成到主工程的时候,需要注意在主工程里声明加入的第三方组件,否则会提示找不到.
-
组件与组件之间的依赖
如果我有一个自己写的组件名为UIHIOSCommon,此时我想要在UIHIOSCommon中引入UIH_RouterModule,则需要在UIHIOSCommon组件的Podfile文件进行修改,并把它放在和UIH_RouterModule组件工程一个目录下,使用相对路径引入:
use_frameworks!
platform :ios, '10.0'
target 'UIH_VIRGO_IOSAPP_Example' do
pod 'UIH_RouterModule', :path => '../../UIH_RouterModule'
pod 'UIHIOSCommon', :path => '../../UIHIOSCommon'
target 'UIH_VIRGO_IOSAPP_Tests' do
inherit! :search_paths
end
end
这样就会在UIHIOSCommon组件工程的Development Pods文件夹下引入UIH_RouterModule组件,因为是相对引入,所以代码改动是同步的.
但是要注意: 每次修改组件的内容都需要pod install去更新一下,其他组件或者本身的测试代码才能使用。
时间原因,先写到这里 后续规划: