美文网首页GitHub 中文社区iOS#iOS#HeminWon
YTKNetwork集成教程以及相关问题思考

YTKNetwork集成教程以及相关问题思考

作者: 天清水蓝 | 来源:发表于2016-10-17 10:09 被阅读7599次
placeholder

YTKNetwork介绍

YTKNetwork 是猿题库 iOS 研发团队基于 AFNetworking 封装的 iOS 网络库,其实现了一套 High Level 的 API,提供了更高层次的网络访问抽象。目前在 GitHub 上已有 3600+ star ,是 Network 中的新星。

YTKNetwork提供的主要功能

  • 支持按时间缓存和版本号缓存网络请求内容
  • 支持统一设置服务器和 CDN 的地址
  • 支持检查返回 JSON 内容的合法性
  • 支持 blockdelegate 两种模式的回调方式
  • 支持批量的网络请求发送,并统一设置它们的回调(实现在 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'

集成至项目

项目文件介绍

YTKNetwork源码

YTKBaseRequest:为请求的基类,内部声明了请求的常用 API :
比如请求方式,请求解析方式,响应解析方式,请求参数等等。它的用意是让子类去实现的,本身不做实现。

YTKRequest:是 YTKBaseRequest 的子类,在其基础上支持了缓存,并且提供了丰富的缓存策略。基本上项目中使用都是继承于 YTKRequest 去写业务的 Request。

YTKNetworkAgent:真正做网络请求的类,在内部跟 AFNetworking 直接交互,调用了 AFNetworking 提供的各种请求,当然,如果底层想切换其他第三方,在这个类中替换掉就行了。

YTKNetworkConfig:该文件为网络请求的统一配置类,提供了设置 baseUrl cdnUrl 等基础请求路径,可以给所有的请求增加参数等等。

YTKBatchRequest:为批量进行网络请求而生,提供了代理和 block 两种方式给外部使用

YTKChainRequest:当多个请求之间有关联的时候采用此类去实现非常方便,即下一个请求可能要根据上个请求返回的数据进行请求。

YTKBatchRequestAgent,YTKChainRequestAgent:分别是 YTKBatchRequestYTKChainRequest 的操作类,不需要也无妨主动调用

集成文件介绍

My Project Table

这是 Demo 工程的我新增的文件,一般情况下,不建议直接继承于 YTKRequest 类去写业务,需要自己写请求的基类,具体业务请求再继承于改项目基类,避免因新版本 YTKRequest 中修改了部分实现的默认值导致的程序需要做大量的修改。其中:ZCBaseRequestZCBatchRequestZCChainRequest 就是 demo 项目的基类。ZCJSONModel 是 JSON 转 Model 的基类,而 ZCHTTPError 是用于自定义错误信息的

single http example

这是 Demo 工程中具体某个请求的实例。这种展现方式很清晰,ZCGetInfoParam 是请求的入参类,ZCMeGetInfoManger是具体的请求操作类, ZCGetInfoModel是出参类。不过如果入参和出参很少,可以只有一个manger类

相关问题思考

我这里不想介绍 YTKNetwork 的基础和高级使用教程。如果想了解基础以及高级使用教程可以看这里
YTKNetwork 使用基础教程
YTKNetwork 使用高级教程
这篇文章重在介绍集成以及使用过程中遇到一些问题以及解决方案

1>JSON转Model的问题

对于稍微复杂的项目,可能某些接口返回数据有十多个,使用的时候不可能从字典中一个一个读取出来,然后再做 <null> 空处理,一般都是采用转 Model 的方式 转换成具体的业务模型,从业务模型中获取具体数据,常见的有 JSONModelMantleMJExtension 等第三方库,本文以JSONModel为例,来实现框架内部解析成 Model

  1. YTKBaseRequest 新增 JSONModel 属性
/// JsonModel类
@property (nonatomic, strong, readonly, nullable) id   responseJSONModel;
  1. YTKBaseRequest 新增 modelClass 函数,用于子类去实现,表明要转换的具体 model 类的类名
YTKBaseRequest.h

/// model对应的类,子类实现的话会直接映射到该model类并进行初始化操作 
- (Class)modelClass;

 YTKBaseRequest.m
 
- (Class)modelClass
{
    return nil;
}
  1. 查看源码不难发现,真正处理网络请求成功和失败的地方是 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);
   }
}
  1. 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
{
    ///跟第二步的实现方式一样
}
  1. 到这里基本上已经实现了 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;
    }
    //只留关键性代码
}

不难发现,这里的错误其实分三类

  1. requestError:请求错误,为 AFNetworking 进行网络请求的请求错误,比如说没网络。
  2. serializationError:响应错误,为 AFNetworking 响应错误,比如返回的json数据你却用了xml解析,还有很多情况等等。
  3. validationError:校验 json 错误,这里包括 [request statusCodeValidator][request jsonValidator 两种类型的错误,前者为返回的 statusCode 不在你指定的成功请求区间内,后者为返回的 json 数据 跟你重载的 jsonValidator 函数中存在字段不一致的情况。
  4. [可选] 如果你用 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自带了一套插件机制,用于处理 YTKBaseRequestYTKBatchRequestYTKChainRequest 这几种请求的loading展示机制,只需要传入 animatingViewanimatingText 即可。对于弹出统一的错误提示,可以在 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]];
        
    }];
}

这是我个人总结的这几点,如果有更好的方案也可以跟我一起探讨

相关文章

  • YTKNetwork

    YTKNetwork集成教程以及相关问题思考 源码解析之--YTKNetwork网络层 YTKNetwork源码解...

  • YTKNetwork集成教程以及相关问题思考

    DEMO https://github.com/albertjson/SZCEvolution YTKNetwor...

  • YTKNetwork集成教程以及相关问题思考

    YTKNetwork介绍 YTKNetwork 是猿题库 iOS 研发团队基于 AFNetworking 封装的 ...

  • YTKNetwork源码解析2

    YTKNetwork在GitHub的仓库中有一份高级教程,这篇我们就来看下高级教程中相关部分的源码。 YTKUrl...

  • OpenCV:iOS集成

    相关链接:OpenCV官网OpenCV教程 集成相关库 利用cocoapods集成opencv失败 无奈只好去官网...

  • Salesforce中SOAP的实践

    这篇SOAP相关文章是Salesforce开发教程(三)的补充,了解到不少同学在系统集成中会遇到各种各样的问题,之...

  • iOS开发之YTKNetwork 填坑

    相关文章参考:https://github.com/yuantiku/YTKNetwork](https://gi...

  • 集成学习

    问题 1. 什么是集成学习,以及为什么要使用集成学习 2. 集成学习常见思想都有哪些,以及它们都有什么作用 3. ...

  • 产品集成过程域

    本过程域要实现以下目标:1)做好产品集成的准备工作,包括集成策划、环境准备、确认接口,以及相关准则;2)按照集成策...

  • android 执行环境编译后执行报错

    问题描述 我之前的android环境没有问题,后来集成了push的相关操作,中间准备换另一种集成方式。就把之前集成...

网友评论

  • iCodingBoy:不知道楼主说的AF暴露URL是个什么情况,基于AF封装的库并不会暴露url,只需传入参数即可,内部可以自行封装,在我看来猿题库所做的工作并不比AF多多少,http请求无非也就那些些事,相对于直接基于AF封装的接口,ytk的方式更为松散,绝大多数接口对象都是重复性工作,我们公司的API基于ytk封装的,API文件都上千个,查找调用都比较麻烦
    口袋海贼王_:@卞泽 你应该是没接触过大型项目。
    卞泽:同意
    请求无非请求链接、参数、方式不同,其他大部分接口的处理都是一样的。猿题库这种方式需要创建大量重复性文件,麻烦!
    我们只是GitHub的搬运工:非也,项目越是大就越需要模块化拆分,设置好目录接口方便管理,既然你的项目api文件上千个那么接口数量也在几百个以上,基本不可能是独立开发,离散的方式更利于团队协同开发.一个接口对应一个类N个业务,实现了高聚合低耦合.何来重复性工作一说.只不过将你写在业务层的网络请求独立拆分成模块而已.当然业务量是大了,后期维护的成本就小了.你见过一个类打开上千行代码的吗?全TM是接口信息.
  • 指尖猿:求问楼主
    想给YTKNetwork添加一个loding
    比如说,网络发起的时候,增加一个类似HUD的进度条.
    可是我不希望在每个网络请求类里都去写这样一套代码..
  • 小星星吃KFC:上传文件 没有进度吧?? 这个算这个库的不足点吗?
  • aa7c14a38783:- (void)sendChainRequest {
    RegisterApi *reg = [[RegisterApi alloc] initWithUsername:@"username" password:@"password"];
    YTKChainRequest *chainReq = [[YTKChainRequest alloc] init];
    [chainReq addRequest:reg callback:^(YTKChainRequest *chainRequest, YTKBaseRequest *baseRequest) {
    //请教下,这里可以直接用reg吗,不需要使用baseRequest吧?返回的这个参数好像没什么用啊?
    RegisterApi *result = (RegisterApi *)baseRequest;
    NSString *userId = [result userId];
    GetUserInfoApi *api = [[GetUserInfoApi alloc] initWithUserId:userId];
    [chainRequest addRequest:api callback:nil];

    }];
    chainReq.delegate = self;
    // start to send request
    [chainReq start];
    }
    aa7c14a38783:请教下,回调后,可以直接用reg吗,不需要使用baseRequest转成reg吧?返回的这个参数好像没什么用啊?
  • 充电星球:我请求https应该怎么设置呢?一开始报-1106,然后添加了text/html后又报3840,查了半天也还是没解决3840这个问题
  • f1535a71d2d8:我这个请求 预期请求成功的 总是从请求失败里面返回 这个是为啥
    上帝也是码农:你打印一下错误,如果是这个Request failed: unacceptable content-type: text/html,就是AFN不支持,需要在AFURLResponseSerialization.m文件,
    添加 @"text/html"
    添加完的样子:self.acceptableContentTypes = [NSSetsetWithObjects:@"application/json", @"text/html",@"text/json",@"text/JavaScript", nil];
  • 我是王海龙: Demo源码里没发现 network文件夹下的的几个文件啊
    天清水蓝:@让吃货瘦成一道闪电 :stuck_out_tongue_winking_eye:没关系啦,反正有文件就行啦
    我是王海龙:@天清水蓝 文章中给的貌似不是这个地址
    天清水蓝:我确认了一下是有的,demo链接:https://github.com/albertjson/SZCEvolution
  • 吃屁的小栗子:你好,我试了一下并没有发现YTK自带的HUD啊,您能说的详细点么?
    吃屁的小栗子:@天清水蓝 哦 谢谢
    天清水蓝:@吃屁的小栗子 不是自带的,但是你可以在他提供的API里面直接添加你自己的HUD
  • 微笑刺客Fly:您好,您上面写的可以提供一份demo吗?十分感激
  • 0无敌小宋0:我post返回Request failed: unacceptable content-type: text/plain这个错,但是在服务器的这个请求是成功的,以后在这个response里面返还给我了成功字段,这是为什么,怎么解决呢
    0无敌小宋0:@天清水蓝 已修改,返回格式改为json改为http
    天清水蓝:@0无敌小宋0 你把响应的content-type改一下就好了。问下服务端是不是json格式的。或者分别尝试下@"application/json"或@"text/html"
  • 再美也只是回忆:感谢作者分享,请问我们是每一个请求都去建立一个类吗?加入请求接口很多,能否封装到一个类里面呢?
    天清水蓝:@TeMortred 没什么,文件多了找问题才方便,也方便多人开发以及维护
    TeMortred:如果是每一个请求创建一个类,那么一个项目中的网络请求50个的话,不得创建五十个类?包括.h和.m文件就有100个文件,这也太吓人了吧 :no_mouth:
    天清水蓝:@再美也只是回忆 每一个请求都创建一个类,方便后期业务更改,都创建一个类这种是集约式的思路,集成迅速,但不适合大项目:grin:

本文标题:YTKNetwork集成教程以及相关问题思考

本文链接:https://www.haomeiwen.com/subject/kcxnyttx.html