在上一篇《MCCSframework 教程(一)介绍》中,我们介绍了一个“不那么复杂”的例子。在这个例子中,我们搜索商品时使用了 mock (模拟)数据。在真实项目中,这当然是不可能的。APP 的数据一般来自于网络,而MCCSframework 作为一个 APP 构建框架而不仅仅是 UI 框架,当然也包含了网络 API。

框架提供的网络 API

框架提供了一个分类 NSObject+AFN。对于常规的 HTTP JSON 接口,它提供了两个方法:

-(RACSignal*)dataFromUrl:(NSString*)url
                  method:(NSString*)method
                  params:(NSDictionary*)params
                   token:(NSString*)token
               errorFilter:(Class<JSONResponseErrorFilter>)validator
               dataClass:(Class)dataClass
              parseBlock:(id (^)(id data,id resp,NSError** error))parseBlock;
-(RACSignal*)dictionaryFromUrl:(NSString*)url
                method:(NSString*)method
                 param:(NSDictionary*)param
                 token:(NSString*)token
             errorFilter:(Class<JSONResponseErrorFilter>)validator;

dataFromUrl 方法

NSObject+AFN 使用了 ReactiveObjC 框架,方法返回结果已经封装为 RAC 信号,方便你以响应式的方式调用。

首先是 dataFromUrl 方法。这个方法会比第二个方法要做更多的工作,它会把接口返回的 JSON 中的 data 数据取出来发送(调用 sendNext:方法) 给你。如果在这个过程中有任何错误,则发送一个错误(调用 sendError:)。它需要的参数较多,前 4 个参数不用解释,一看就懂。特别的是后面这 3 个参数:

errorFilter 参数

这是一个实现了 JSONResponseErrorFilter 协议的类(注意是类,不是对象)。这个类起一个“错误过滤器”的作用,负责过滤在解析 Response 过程中出现的各种错误,最终返回一个 NSDictionary 对象给你。

JSONResponseErrorFilter 协议在 APIError.h 中定义:

@protocol JSONResponseErrorFilter <NSObject>
+ (id)dictionaryWithResponse:(id)responseObj;// 获取 NSDictionary,如有错误返回错误
+ (id)dataWithResponse:(NSDictionary*)responseDic;// 获取 data 字段,如有错误返回错误
@end

注意,APIError 类也实现了该协议。

这些方法用于描述每种错误应当如何识别和定义。比如用户未登录错误,token 过期等。这些错误对于不同的项目会有不同的定义,在实际项目中,你应该创建自己的 JSONResponseErrorFilter 类,然后根据业务定义来实现这两个方法。例如,你可以这样实现 dictionaryWithResponse 方法:

+(id)dictionaryWithResponse:(id)responseObj{
    if([responseObj isKindOfClass:NSString.class]){
        NSString* str = (NSString*)responseObj;
        if([str containsString:@"please to login"]||[str containsString:@"Request failed: unauthorized (401)"]){
            return [APIError noLoginError];// 915:未登录
        }else if([str containsString:@"This request token is expired"]){
            return [APIError tokenInvalid];// 916:token 无效
        }
        return str;
    }else if([responseObj isKindOfClass:NSDictionary.class]){
        NSDictionary* responseDic =(NSDictionary*)responseObj;
        
        NSNumber* code = responseDic[@"code"];// 如果 code 字段的 key 不是 'code',修改此处
        NSNumber* succeed = responseDic[@"succeed"];// 如果 succeed 字段的 key 不是 'succeed',修改此处
        NSString* msg = responseDic[@"msg"];// 如果 msg 字段的 key 不是 'msg',修改此处
        
        if([code isEqual:@404]){
            return [APIError http404Error];// 918: HTTP 404 错误
        }
        
        if([msg containsString:@"Request failed: unauthorized (401)"]){
            return [APIError noLoginError];// 915:未登录
        }
        
        if(![code isEqual: @0] || succeed.boolValue==NO){
            return stringIsEmpty(msg) ? [APIError statusFailedError]: [NSError errorWithDomain:@"APIError" code:919 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@:%@",code,msg]}];// 919:后台返回状态异常
        }
        return responseDic;
    }else{// NSString 和 NSDictionary 以外的类型不接受
        return [APIError dataParseError];// 913: 服务器返回的数据类型无法解析
    }
}

在这个方法中,你可以识别出后台返回的 response 中包含的各种错误,以上面的代码为例,它主要拦截了以下错误:

  1. 用户未登录

    如果后台接口直接返回 “please to login” 或 “Request failed: unauthorized (401)” 字符串,那么说明用户未登录。

  2. token 过期

    而当后台接口直接返回 “This request token is expired” 字符串,则表明 token 过期了。

    上述两种错误,后台返回的数据格式都是字符串,而不是 JSON。对于后台来说, JSON 接口本来不应该返回除 JSON 以外的数据格式,但在实际开发过程中移动端经常会碰到这种操蛋的问题,这其实是因为后台缺乏经验或粗心大意导致的。你无法确定自己会不会碰到这样的后台开发人员,所以你必须多做一些工作,在自己的代码中防范此类问题。

  3. 404 错误

    后台有时候会把 HTTP 返回码在 JSON 中返回。404 是一种比较特殊的错误,一般表示 url 地址不存在或者错误,因此需要单独列出,以便你能够检查是不是接口地址写错了。

  4. 后台返回不成功状态的错误

    一般后台返回的 JSON 中会有一个状态表示请求是否成功。但很多时候,后台还会用一个返回码(code)和简短信息(msg)表示具体失败的原因是什么。这也是我们唯一能“真正理解”为什么出错的地方。

  5. data 字段缺失错误

    这种错误就真的是由于后台的粗心大意了。在某些时候,后台返回的结果中直接就缺少了 data 字段。当然,data 字段的 key 不一定总是叫 “data”,只要你知道要到哪里查找想要的数据,那么把它替换成什么名字都可以。

dataWithResponse 方法的实现则比较简单,返回 data 字段中存储的对象就可以了,如果 data 字段缺失,返回一个错误:

/// 获取 data 字段,如缺失返回 dataNilError
+ (id)dataWithResponse:(NSDictionary*)responseDic{
    id dataObj = responseDic[@"data"];// 如果 data 字段的 key 不是 'data',修改此处
    if(dataObj==nil)
        return [APIError dataNilError];// 911:data 字段缺失
    else
        return dataObj;
}
dataClass 参数

你必须指定 data 字段的类型。通常后台返回的 JSON 字符串中,查询结果会放在 data 字段中。但根据要查询的数据不同,data 字段有可能被解析为多种类型:字典、数组、字符串。具体讲:

  • 如果前端是通过唯一ID来查询,那么后台只可能会查询到单条记录,而且后台很可能直接将这条记录放到 data 字段,那么 data 字段就应该被解析为一个 NSDictionary 类型。
  • 如果前端根据其他条件查询,那么后台查询结果可能是多条记录,那么后台很可能会在 data 字段中放入数组,前端应当把 data 字段识别为 NSArray 类型。
  • 很少的情况下,后台会直接在 data 字段中放入字符串,比如一个 token 值。那么 dataClass 参数应当指定为 NSString 类型。
parseBlock 参数

data 字段的具体解析过程在这个 block 中进行。在 MCCSframework 中,网络请求大部分代码已经封装好,我们唯一需要关心的部分就是数据的解析。在 dataParseBlock 块中,我们可以对 JSON 字符串中的 data 字段进行解析,其实在这个块中的代码通常都是非常简单的,一般只有 2 行代码,比如:

GoodsTypeDetail *detail = [[GoodsTypeDetail alloc]initWithDictionary:data error:error];
return detail;

dictionaryFromUrl 方法

第二个方法是 dictionaryFromUrl 方法,它比起第一个方法来省略了后两个参数。因为在某些情况下,我们对服务器的请求不是查询而是更新操作时,我们并不需要关心 data 字段的内容,也不需要对它进行解析,这样 dataClass 和 dataParseBlock 参数就可以省略了。比如删除或修改某条记录时。

其它方法

对于一般的 JSON 格式的网络接口,上面两个方法足够使用。如果接口的数据格式不是 JSON,或者上两个方法不能满足你的使用,那么 NSObject+AFN 还提供了另外两个非RAC 信号封装的、使用传统 block 回调的方法,可以满足你对任意 HTTP/HTTPs 接口的调用:

-(NSURLSessionDataTask*)postUrl:(NSString *)url
                         params:(id)params
                        headers:(NSDictionary<NSString *,NSString*>*)headers
                    requestType:(NSString *)requestType success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
                        failure:(void (^)(NSURLSessionDataTask* task, NSError *error))failure;
-(NSURLSessionDataTask*)getUrl:(NSString*)url
                        params:(NSDictionary<NSString *,id>*)params
                       headers:(NSDictionary<NSString *,NSString*>*)headers
                       success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
                       failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;

这两个方法分别用于 HTTP/HTTPs 的 POST 和 GET 请求。

封装 NSObject+AFN

为了便于使用,我们可以在框架提供的 API 的基础上进一步封装网络接口。进一步的封装需要根据项目的具体业务进行调整,不同的项目其封装代码可能不一样。这里我们演示一个具体的例子,你可以根据自己的项目需求进行调整。

新建一个分类 NSObject+RAC_HTTP,声明两个方法。定义如下:

#import <ReactiveObjC.h>

@interface NSObject (RAC_HTTP)

// 需要解析 data JSON 的调用此 API
-(RACSignal*)signalFromUrl:(NSString*)url method:(NSString*)method params:(NSDictionary*)params dataClass:(Class)dataClass dataParseBlock:(id (^)(id data,id resp,NSError** error))dataParseBlock;
// 不需要解析 data JSON 的调用此 API
-(RACSignal*)signalFromUrl:(NSString*)url method:(NSString*)method params:(NSDictionary*)params;
@end

这两个方法分别在 NSObject+AFN 分类的 dataFromUrl 方法和 dictionaryFromUrl 方法基础上进行了封装,进一步减少了 token 参数和 validator 参数。

NSObject+RAC_HTTP 分类的实现如下:

#import "NSObject+RAC_HTTP.h"
#import <MCCSframework/NSString+Add.h>
#import <MCCSframework/NSObject+AFN.h>
#import <MCCSframework/Utils.h>
#import <MCCSframework/APIError.h>
#import "BaseModel.h"
#import "AppDelegate.h"
#import <MCCSframework/APIError.h>

@implementation NSObject (RAC_HTTP)

// 需要解析 data 字段的调用此方法
-(RACSignal*)signalFromUrl:(NSString*)url method:(NSString*)method params:(NSDictionary*)params dataClass:(Class)dataClass dataParseBlock:(id (^)(id data,id resp,NSError** error))dataParseBlock{
    NSString* token = appDelegate.auth.token;
    RACSignal* signal = [self dataFromUrl:url method:method params:params token:token errorFilter:APIError.class dataClass:dataClass parseBlock:dataParseBlock];
    
    return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        return [signal subscribeNext:^(id x) {
            [subscriber sendNext:x];
            [subscriber sendCompleted];
        } error:^(NSError * _Nullable error) {
            if(error.code == 916 || error.code == 915){// 拦截 token 无效的错误
                [appDelegate gotoLoginVC];
            }else{
                [subscriber sendError:error];
            }
        }];
    }];
}
// 不需要解析 data 字段的调用此方法
-(RACSignal*)signalFromUrl:(NSString*)url method:(NSString*)method params:(NSDictionary*)params{
    NSString* token = appDelegate.auth.token;
    RACSignal* signal = [self dictionaryFromUrl:url method:method param:params token:token errorFilter:APIError.class];
    return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        return [signal subscribeNext:^(NSDictionary* x) {
            NSError* error;
            BaseModel* bm = [[BaseModel alloc]initWithDictionary:x error:&error];
            
            if(error){
                [subscriber sendError:APIError.dataParseError];
            }else if(bm.code == 0){
                [subscriber sendNext:bm];
                [subscriber sendCompleted];
            }
        } error:^(NSError * _Nullable error) {
            if(error.code == 916 || error.code == 915){// 拦截 token 无效的错误
                [appDelegate gotoLoginVC];
            }else{
                [subscriber sendError:error];
            }
        }];
    }];
}
@end

要在你自己的工程中使用该分类,需要对代码进行一些修改。例如:

  1. 错误处理

    注意这一句,if(error.code == 916 || error.code == 915),这是因为当 token 过期或无效时,一般情况下,服务器后台会返回 915/916 错误,所以这里需要引导用户回到登录页重新登录。但对于不同的服务器后台,这个情况可能会不一样,因此你可能需要修改这部分的代码。

    此外,注意这一句:

     RACSignal* signal = [self dataFromUrl:url method:method params:params token:token validator:APIError.class dataClass:dataClass parseBlock:dataParseBlock];
    

    validator 参数我们使用的是 APIError 类,这是框架默认的错误定义类。实际上你应该在这里替换成自己的 JSONResponseErrorFilter 协议实现类,因为每个项目中对于接口错误的定义是不一样的。

  2. BaseModel 类

    一个 JKModel 子类。对于一般的后台而言,它们返回的 JSON 格式是基本固定的,比如都是这样的:

    {
    "succeed": true,
    "code": 0,
    "msg": "查询成功",
    "data": ...
    }
    

    唯一会变化的可能是 data 字段,根据不同的查询 data 字段会返回不同类型的数据:字典、数组或字符串。
    因此 BaseModel 会定义成这样:

     @interface BaseModel : JKModel
     @property (strong, nonatomic) NSString* msg;
     @property(assign, nonatomic) NSInteger code;
     @property(assign, nonatomic) BOOL succeed;
     @end
    

    其中 3 个固定字段都定义在 BaseModel 中了。因此在第一个方法中,可以先解析出 BaseModel,然后将 data 字段传给 dataParseBlock 块去解析。

    BaseModel 不需要定义 data 属性,因为不关心 data 字段,data 字段的解析交给 dataParseBlock 块进行解析。

  3. token

    注意这句 NSString* token = appDelegate.auth.token;,当用户登录成功后,我们会把 token 保存到 AppDelegate 的 auth 属性中,因此我们可以通过 AppDelegate 的 auth.token 访问到它。你的项目也许会将 token 存放在不同的地方,因此这里的代码也需要根据实际情况进行修改。

使用 NSObject+RAC_HTTP 请求服务器数据

以上一篇中的二级分类商品列表为例。假设后台给我们的接口是这样的:

因为这个接口支持分页数据,因此参数中除了二级分类 ID 外,还会有 2 个分页相关数据:

  • pageSize 页大小
  • pageCurrent 页码

那么我们可以编写查询这个接口的方法:

#import <MCCSframework/NSObject+toDictionary.h>

@implementation MallAPI
+(RACSignal*)goodsPageList:(GoodsQuery*)query{

	NSString* url = @"http://127.0.0.1/mallProductsInfo/getProductList";
	return [self signalFromUrl:url method:@"post" params:[query toDictionary:query containSuper:YES] dataClass:[NSDictionary class] dataParseBlock:^id(id data, id resp, NSError *__autoreleasing *error) {
        
        GoodsPage* page = [[GoodsPage alloc]initWithDictionary:data error:error];
        return page;
    }];
}
@end

其中 GoodsQuery 封装了查询参数:

@interface GoodsQuery : NSObject

@property(assign, nonatomic) NSInteger pageCurrent;// 页号
@property(assign, nonatomic) NSInteger pageSize;// 页大小
@property (strong, nonatomic) NSString* categoryId;// 分类Id

@end

GoodsPage 封装了对 data 字段解析后的对象:

#import <UIKit/UIKit.h>
#import <MCCSframework/JKModel.h>

@class Goods;

@interface GoodsPage : NSObject
@property(assign, nonatomic) NSInteger size;// 页大小
@property(assign, nonatomic) NSInteger current;// 页码
@property (assign, nonatomic) NSInteger pages;// 总页数
@property(assign, nonatomic) NSInteger total;// 总记录数
@property (strong, nonatomic) NSArray<Goods*>* records;
@end
    
@interface Goods : JKModel
@property (strong, nonatomic) NSString* id;
@property (strong, nonatomic) NSString* categoryId;
@property (strong, nonatomic) NSString* productName; // 商品名称
@property (strong, nonatomic) NSString* introduction;// 文字介绍
@property (strong, nonatomic) NSString* imgId;// 商品图片
@property(assign, nonatomic) CGFloat price;// 价格
@property (strong, nonatomic) NSString* priceUnit;// 单位

...... 

@end

然后在 ViewController 中这样调用 goodsPageList 方法:

	  @weakify(self)
    [[MallAPI goodsPageList:query] subscribeNext:^(GoodsPage* x) {
        @strongify(self)
        // 1
        [self.optimumSC.goodsArray addObjectsFromArray:x.records];
        // 2
        [self.optimumSC.collectionContext  reloadSectionController:self.optimumSC];
    } error:^(NSError * _Nullable error) {
        [self showHint:error.localizedDescription];
    }];

  1. x 是 GoodsPage 类型,它的 records 属性包含了查询到的商品数据,我们将数据添加到 optimumSC(这是一个 SubController)的 goodsArray 数组中。
  2. 然后刷新 SubController,查询到的商品立即显示到列表中。

结束

网络 API 实际上是你和后台交互的过程。但后台开发人员不是上帝,他们和你一样,也经常会犯这样那样的错误。所以在网络 API 中,大量的代码用于处理“出了什么错”以及“错误在哪里”的问题上。

MCCSframework 框架为了解决这个问题,引入了 JSONResponseErrorFilter 协议用于通过 Response 来定义各种错误。在所有的错误中,后台返回的状态是非常重要的,具有重要的参考意义,这是后台在明确告诉我们 “为什么失败” 了。但除此之外的所有错误都是不明确的,需要我们根据每个后台的实际情况进行识别和定义。因此在不同项目中,实现 JSONResponseErrorFilter 协议的实现是不一样的,每个错误的定义要因“人”而异。

当然,如果你遇到一个“好的后台”,所有错误都被后台定义了,那么JSONResponseErrorFilter 类中需要做的工作会很少。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐