YTKNetwork介绍
YTKNetwork 是猿题库 iOS 研发团队基于 AFNetworking 封装的 iOS 网络库,其实现了一套 High Level 的 API,提供了更高层次的网络访问抽象。目前在 GitHub 上已有 3600+ star ,是 Network 中的新星。
YTKNetwork提供的主要功能
- 支持按时间缓存和版本号缓存网络请求内容
- 支持统一设置服务器和 CDN 的地址
- 支持检查返回 JSON 内容的合法性
- 支持
block
和delegate
两种模式的回调方式 - 支持批量的网络请求发送,并统一设置它们的回调(实现在
YTKBatchRequest
类中) - 支持方便地设置有相互依赖的网络请求的发送,例如:发送请求 A,根据请求 A 的结果,选择性的发送请求 B 和 C,再根据 B 和 C 的结果,选择性的发送请求 D。(实现在
YTKChainRequest
类中) - 支持网络请求 URL 的 filter,可以统一为网络请求加上一些参数,或者修改一些路径。
- 定义了一套插件机制,可以很方便地为 YTKNetwork 增加功能。猿题库官方现在提供了一个插件,可以在某些网络请求发起时,在界面上显示“正在加载”的 HUD。
YTKNetwork 的基本思想
YTKNetwork 的基本的思想是把每一个网络请求封装成对象。所以使用 YTKNetwork,你的每一个请求都需要继承 YTKRequest 类,通过覆盖父类的一些方法来构造指定的网络请求。
把每一个网络请求封装成对象其实是使用了设计模式中的 Command 模式,它有以下好处:
- 将网络请求与具体的第三方库依赖隔离,方便以后更换底层的网络库。
- 方便在基类中处理公共逻辑,例如猿题库的数据版本号信息就统一在基类中处理。
- 方便在基类中处理缓存逻辑,以及其它一些公共逻辑。
方便做对象的持久化。
当然,如果说它有什么不好,那就是如果你的工程非常简单,这么写会显得没有直接用 AFNetworking
将请求逻辑写在 Controller 中方便,所以 YTKNetwork 并不合适特别简单的项目。
关于集约式和离散式
集约式
介绍:即项目中的每个请求都会走统一的入口,对外暴露了请求的 URL 和 Param 以及请求方式,入口一般都是通过单例
来实现,AFNetworking 的官方 demo 就是采用的集约式的方式对网络请求进行的封装,也是目前比较流行的网络请求方式。
优点:
- 使用便捷,能实现快速开发
缺点:
- 对每个请求的定制型不够强
- 不方便后期业务拓展
离散式
介绍:即每个网络请求类都是一个对象,它的 URL 以及请求方式和响应方式 均不暴露给外部调用。只能内部通过 重载或实现协议
的方式来指定,外部调用只需要传 Param 即可,YTKNetwork就是采用的这种网络请求方式。
优点:
- URL 以及请求和响应方式不暴露给外部,避免外部调用的时候写错
- 业务方使用起来较简单,业务使用者不需要去关心它的内部实现
- 可定制性强,可以为每个请求指定请求的超时时间以及缓存的周期
缺点:
- 网络层需要业务实现方去写,变相的增加了部分工作量
- 文件增多,程序包会变大[倒也不是特别大]
在微脉的iOS客户端,由于最初人员较少,且业务变更较频繁。故使用的就是集约式请求。不过考虑到为实现业务便捷性以及可拓展性,故增加了 RequestHeader
请求头,以及 WMHttpHelper
网络操作工具类。基本上已满足于目前的开发模式
不过长远来看,转成离散式的网络请求也是有必要的。
安装
你可以在 Podfile 中加入下面一行代码来使用 YTKNetwork
pod 'YTKNetwork'
集成至项目
项目文件介绍
YTKBaseRequest:为请求的基类,内部声明了请求的常用 API :
比如请求方式,请求解析方式,响应解析方式,请求参数等等。它的用意是让子类去实现的,本身不做实现。
YTKRequest:是 YTKBaseRequest
的子类,在其基础上支持了缓存,并且提供了丰富的缓存策略。基本上项目中使用都是继承于 YTKRequest
去写业务的 Request。
YTKNetworkAgent:真正做网络请求的类,在内部跟 AFNetworking
直接交互,调用了 AFNetworking
提供的各种请求,当然,如果底层想切换其他第三方,在这个类中替换掉就行了。
YTKNetworkConfig:该文件为网络请求的统一配置类,提供了设置 baseUrl
cdnUrl
等基础请求路径,可以给所有的请求增加参数等等。
YTKBatchRequest:为批量进行网络请求而生,提供了代理和 block 两种方式给外部使用
YTKChainRequest:当多个请求之间有关联的时候采用此类去实现非常方便,即下一个请求可能要根据上个请求返回的数据进行请求。
YTKBatchRequestAgent,YTKChainRequestAgent:分别是 YTKBatchRequest
,YTKChainRequest
的操作类,不需要也无妨主动调用
集成文件介绍
这是 Demo 工程的我新增的文件,一般情况下,不建议直接继承于 YTKRequest
类去写业务,需要自己写请求的基类,具体业务请求再继承于改项目基类,避免因新版本 YTKRequest 中修改了部分实现的默认值导致的程序需要做大量的修改。其中:ZCBaseRequest
,ZCBatchRequest
,ZCChainRequest
就是 demo 项目的基类。ZCJSONModel
是 JSON 转 Model 的基类,而 ZCHTTPError
是用于自定义错误信息的
这是 Demo 工程中具体某个请求的实例。这种展现方式很清晰,ZCGetInfoParam
是请求的入参类,ZCMeGetInfoManger
是具体的请求操作类, ZCGetInfoModel
是出参类。不过如果入参和出参很少,可以只有一个manger类
相关问题思考
我这里不想介绍 YTKNetwork
的基础和高级使用教程。如果想了解基础以及高级使用教程可以看这里
YTKNetwork 使用基础教程
YTKNetwork 使用高级教程
这篇文章重在介绍集成以及使用过程中遇到一些问题以及解决方案
1>JSON转Model的问题
对于稍微复杂的项目,可能某些接口返回数据有十多个,使用的时候不可能从字典中一个一个读取出来,然后再做 <null>
空处理,一般都是采用转 Model 的方式 转换成具体的业务模型,从业务模型中获取具体数据,常见的有 JSONModel
,Mantle
,MJExtension
等第三方库,本文以JSONModel
为例,来实现框架内部解析成 Model
- 在
YTKBaseRequest
新增 JSONModel 属性
/// JsonModel类
@property (nonatomic, strong, readonly, nullable) id responseJSONModel;
- 在
YTKBaseRequest
新增 modelClass 函数,用于子类去实现,表明要转换的具体 model 类的类名
YTKBaseRequest.h
/// model对应的类,子类实现的话会直接映射到该model类并进行初始化操作
- (Class)modelClass;
YTKBaseRequest.m
- (Class)modelClass
{
return nil;
}
- 查看源码不难发现,真正处理网络请求成功和失败的地方是
YTKNetworkAgent
类,在- (void)handleRequestResult:(NSURLSessionTask *)task responseObject:(id)responseObject error:(NSError *)error
和- (void)requestDidSucceedWithRequest:(YTKBaseRequest *)request
,- (void)requestDidFailWithRequest:(YTKBaseRequest *)request error:(NSError *)error
这三个方法。
具体操作为
- (void)requestDidSucceedWithRequest:(YTKBaseRequest *)request {
@autoreleasepool {
[request requestCompletePreprocessor];
[self JSONConvertModel:request];
}
dispatch_async(dispatch_get_main_queue(), ^{
[request toggleAccessoriesWillStopCallBack];
[request requestCompleteFilter];
if (request.delegate != nil) {
[request.delegate requestFinished:request];
}
if (request.successCompletionBlock) {
request.successCompletionBlock(request);
}
[request toggleAccessoriesDidStopCallBack];
});
}
///json转model的具体方法
- (void)JSONConvertModel:(YTKBaseRequest*)request
{
Class modelClass = [request modelClass];
if (!modelClass) {
return;
}
NSError * error = nil;
if ([request.responseJSONObject isKindOfClass:[NSDictionary class]]) {
request.responseJSONModel = [[modelClass alloc] initWithDictionary:request.responseJSONObject error:&error];
}else if ([request.responseJSONObject isKindOfClass:[NSArray class]]){
request.responseJSONModel = [modelClass arrayOfModelsFromDictionaries:request.responseJSONObject error:&error];
}else if {
//这里不做处理,因为AFNetworking如果返回的数据为null的时候会调用失败的回调
}
if (error) {
YTKLog(@"Request JSON---JSONModel Failed =%@",error);
}
}
- 在
YTKRequest
类中也需要新增缓存类的model,具体代码为
YTKRequest.m
@property (nonatomic, strong) id cacheJSONModel;///TTT
- (id)responseJSONModel {
if (_cacheJSONModel) {
return _cacheJSONModel;
}
return [super responseJSONModel];
}
- (BOOL)loadCacheData {
NSString *path = [self cacheFilePath];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
if ([fileManager fileExistsAtPath:path isDirectory:nil]) {
NSData *data = [NSData dataWithContentsOfFile:path];
_cacheData = data;
_cacheString = [[NSString alloc] initWithData:_cacheData encoding:self.cacheMetadata.stringEncoding];
switch (self.responseSerializerType) {
case YTKResponseSerializerTypeHTTP:
// Do nothing.
return YES;
case YTKResponseSerializerTypeJSON:
_cacheJSON = [NSJSONSerialization JSONObjectWithData:_cacheData options:(NSJSONReadingOptions)0 error:&error];
if (!error) {
[self JSONConvertModel:_cacheJSON];
}
return error == nil;
case YTKResponseSerializerTypeXMLParser:
_cacheXML = [[NSXMLParser alloc] initWithData:_cacheData];
return YES;
}
}
return NO;
}
- (void)JSONConvertModel:(YTKBaseRequest*)request
{
///跟第二步的实现方式一样
}
- 到这里基本上已经实现了 json-model,具体的业务代码为:
- (void)loadCacheData {
NSString *userId = @"1";
GetUserInfoApi *api = [[GetUserInfoApi alloc] initWithUserId:userId];
if ([api loadCacheWithError:nil]) {
NSDictionary *json = [api responseJSONObject];
NSLog(@"json = %@", json);
// show cached data
YTKJSONModel * model = [api responseJSONModel];
NSLog(@"jsonmodelllll=%@---%@",model.nick,model.level);
}
api.animatingText = @"正在加载";
api.animatingView = self.view;
[api startWithCompletionBlockWithSuccess:^(YTKBaseRequest *request) {
NSLog(@"update ui=%@",[api responseJSONModel]);
} failure:^(YTKBaseRequest *request) {
NSLog(@"failed");
}];
}
大致步骤如此,只是这样实现的话需要修改源代码,细节可以参考 demo,地址:https://github.com/albertjson/YTKNetwork
2>token引发的问题
一般情况下,网络请求客户端都要带 token,用于服务端验证用户的登陆有效性。那么 token 失效可能需要做一些处理,在 demo 中这部分验证是写在 ZCBaseRequest
类中实现的。这样避免业务代码在各处进行处理 token 失效的情况
- (void)requestFailedFilter
{
[super requestFailedFilter];
if (error.code==TokenTimeOut) {
......
}
}
当然,这样处理之后,如果子类需要在错误的时候做特殊处理,那么在重写 requestFailedFilter
方法的时候一定要调用 [super requestFailedFilter]
3>错误解析
YTKNetwork 调用 HTTP 返回错误的类为 NSError。而自己的项目一般都需要定制错误信息,或者根据某一类型的错误进行特殊的操作。这一步可以在自己定义的请求基类的错误回调中处理。我们先来看一段 YTKNetwork 的源码:
- (void)handleRequestResult:(NSURLSessionTask *)task responseObject:(id)responseObject error:(NSError *)error {
//这里只留下关键性代码
NSError * __autoreleasing serializationError = nil;
NSError * __autoreleasing validationError = nil;
NSError *requestError = nil;
BOOL succeed = NO;
request.responseObject = responseObject;
if ([request.responseObject isKindOfClass:[NSData class]]) {
request.responseData = responseObject;
request.responseString = [[NSString alloc] initWithData:responseObject encoding:[YTKNetworkUtils stringEncodingWithRequest:request]];
switch (request.responseSerializerType) {
case YTKResponseSerializerTypeHTTP:
// Default serializer. Do nothing.
break;
case YTKResponseSerializerTypeJSON:
request.responseObject = [self.jsonResponseSerializer responseObjectForResponse:task.response data:request.responseData error:&serializationError];
request.responseJSONObject = request.responseObject;
break;
case YTKResponseSerializerTypeXMLParser:
request.responseObject = [self.xmlParserResponseSerialzier responseObjectForResponse:task.response data:request.responseData error:&serializationError];
break;
}
}
if (error) {
succeed = NO;
requestError = error;
} else if (serializationError) {
succeed = NO;
requestError = serializationError;
} else {
succeed = [self validateResult:request error:&validationError];
requestError = validationError;
}
//只留关键性代码
}
不难发现,这里的错误其实分三类
- requestError:请求错误,为 AFNetworking 进行网络请求的请求错误,比如说没网络。
- serializationError:响应错误,为 AFNetworking 响应错误,比如返回的json数据你却用了xml解析,还有很多情况等等。
-
validationError:校验 json 错误,这里包括
[request statusCodeValidator]
和[request jsonValidator
两种类型的错误,前者为返回的 statusCode 不在你指定的成功请求区间内,后者为返回的 json 数据 跟你重载的 jsonValidator 函数中存在字段不一致的情况。 - [可选] 如果你用 JSONModel 还会有 JSONModel 解析错误产生的错误。
处理方式如下:
//这里暂不考虑JSONModel解析错误的问题
- (void)requestFailedPreprocessor
{
//note:子类如需继承,必须必须调用 [super requestFailedPreprocessor];
[super requestFailedPreprocessor];
NSError * error = self.error;
if ([error.domain isEqualToString:AFURLResponseSerializationErrorDomain])
{
//AFNetworking处理过的错误
}else if ([error.domain isEqualToString:YTKRequestValidationErrorDomain])
{
//猿题库处理过的错误
}else{
//系统级别的domain错误,无网络等[NSURLErrorDomain]
//根据error的code去定义显示的信息,保证显示的内容可以便捷的控制
}
}
这里还有一种特殊情况,就是服务端返回的错误不一定是以 错误
的方式给你。可能请求状态码依然是200OK,那么这个时候需要重写 YTK 提供的成功和失败的block和重写代理
4>loading动画以及错误弹出机制
YTK自带了一套插件机制,用于处理 YTKBaseRequest
,YTKBatchRequest
,YTKChainRequest
这几种请求的loading展示机制,只需要传入 animatingView
和 animatingText
即可。对于弹出统一的错误提示,可以在 ZCBaseRequest
的失败主线程回调中进行。即:
/// Called on the main thread when request failed.
- (void)requestFailedFilter
{
[super requestFailedFilter];
if (![self isHideErrorToast]) {
UIWindow * window = [[UIApplication sharedApplication] keyWindow];
UIViewController * controller = [self findBestViewController:window.rootViewController];
[WMHUDUntil showFailWithMessage:self.error.localizedDescription toView:controller.view];
}
}
其中 [self isHideErrorToast]
用于表示是否隐藏错误提示。该方法由具体的子类去实现。
5>网络请求的终止
YTK给出网络请求关闭方案:在dealloc中调用:
Remove self from request queue and cancel the request.
- (void)stop;
所以,建议每个网络请求都在controller写成全局的变量。
下面展示一下具体某个请求的代码:
ZCTYKTestViewController.m
@interface ZCTYKTestViewController ()
@property (nonatomic,strong) ZCMeGetInfoManger * infoManger;
@end
@implementation ZCTYKTestViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setupData];
}
- (void)setupData
{
self.infoManger = [[ZCMeGetInfoManger alloc] init];
self.infoManger.animatingView = self.view;
}
- (IBAction)buttonAction:(UIButton*)sender
{
[self clearTextView];
ZCGetInfoParam * param = [[ZCGetInfoParam alloc] init];
param.userId = @"0";
param.token = @"222222";
_infoManger.param = param;
[_infoManger startWithCompletionBlockWithSuccess:^(__kindof YTKBaseRequest * _Nonnull request) {
NSLog(@"responseJSONObject=%@",_infoManger.responseJSONObject);
ZCGetInfoModel * infoModel = [[ZCGetInfoModel alloc] initWithDictionary:_infoManger.responseJSONObject error:nil];
[self updateTextViewWithLog:[NSString stringWithFormat:@"读取数据:\n%@",infoModel]];
} failure:^(__kindof YTKBaseRequest * _Nonnull request) {
[weakself updateTextViewWithLog:[NSString stringWithFormat:@"读取失败:\n%@",weakself.infoManger.error]];
}];
}
这是我个人总结的这几点,如果有更好的方案也可以跟我一起探讨