MCCSframework 教程(二)网络 API
在上一篇《MCCSframework 教程(一)介绍》中,我们介绍了一个“不那么复杂”的例子。在这个例子中,我们搜索商品时使用了 mock (模拟)数据。在真实项目中,这当然是不可能的。APP 的数据一般来自于网络,而MCCSframework 作为一个 APP 构建框架而不仅仅是 UI 框架,当然也包含了网络 API。框架提供的网络 API框架提供了一个分类 NSObject+AFN。对于常..
在上一篇《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 中包含的各种错误,以上面的代码为例,它主要拦截了以下错误:
-
用户未登录
如果后台接口直接返回 “please to login” 或 “Request failed: unauthorized (401)” 字符串,那么说明用户未登录。
-
token 过期
而当后台接口直接返回 “This request token is expired” 字符串,则表明 token 过期了。
上述两种错误,后台返回的数据格式都是字符串,而不是 JSON。对于后台来说, JSON 接口本来不应该返回除 JSON 以外的数据格式,但在实际开发过程中移动端经常会碰到这种操蛋的问题,这其实是因为后台缺乏经验或粗心大意导致的。你无法确定自己会不会碰到这样的后台开发人员,所以你必须多做一些工作,在自己的代码中防范此类问题。
-
404 错误
后台有时候会把 HTTP 返回码在 JSON 中返回。404 是一种比较特殊的错误,一般表示 url 地址不存在或者错误,因此需要单独列出,以便你能够检查是不是接口地址写错了。
-
后台返回不成功状态的错误
一般后台返回的 JSON 中会有一个状态表示请求是否成功。但很多时候,后台还会用一个返回码(code)和简短信息(msg)表示具体失败的原因是什么。这也是我们唯一能“真正理解”为什么出错的地方。
-
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
要在你自己的工程中使用该分类,需要对代码进行一些修改。例如:
-
错误处理
注意这一句,
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 协议实现类,因为每个项目中对于接口错误的定义是不一样的。
-
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 块进行解析。
-
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];
}];
- x 是 GoodsPage 类型,它的 records 属性包含了查询到的商品数据,我们将数据添加到 optimumSC(这是一个 SubController)的 goodsArray 数组中。
- 然后刷新 SubController,查询到的商品立即显示到列表中。
结束
网络 API 实际上是你和后台交互的过程。但后台开发人员不是上帝,他们和你一样,也经常会犯这样那样的错误。所以在网络 API 中,大量的代码用于处理“出了什么错”以及“错误在哪里”的问题上。
MCCSframework 框架为了解决这个问题,引入了 JSONResponseErrorFilter 协议用于通过 Response 来定义各种错误。在所有的错误中,后台返回的状态是非常重要的,具有重要的参考意义,这是后台在明确告诉我们 “为什么失败” 了。但除此之外的所有错误都是不明确的,需要我们根据每个后台的实际情况进行识别和定义。因此在不同项目中,实现 JSONResponseErrorFilter 协议的实现是不一样的,每个错误的定义要因“人”而异。
当然,如果你遇到一个“好的后台”,所有错误都被后台定义了,那么JSONResponseErrorFilter 类中需要做的工作会很少。
更多推荐
所有评论(0)