LSYNetworking:基于AFNetworking的网络请
给大家分享一下我封装的一个基于AFNetworking的网络请求库 LSYNetworking。
这个库的特点是,非常简单!非常的容易上手!但是却...非常的强大!
这里的强大,并不是说这个库使用了多么高深的技术(高深的都在AFN里😒),而是说,这个库从广大开发者的实际使用需求出发,考虑到了多种业务需求,将大部分工作(重复的工作)都替开发者做了,只需要简单的几行代码,就可以完成一次网络请求,而你想实现的复杂功能,它几乎都支持。
官方一点的说就是:
LSYNetworking实现了对AFNetworking的高度封装和对网络请求的高度抽象,可应对各种复杂的业务需求,使用者可根据自身的业务需求,在请求过程中的一些关键节点插入自己的逻辑,处理请求的各种数据等
这一篇主要讲一些基础的使用,更多内容可以看一下进阶篇
下面是详细的使用说明
创建你的BaseRequest
首先,我们需要有个BaseRequest,继承自LSYBaseRequest,这个BaseRequest将会是你所有请求的基类,一切拓展功能都将会在这里进行添加。
假设我们创建了YourBaseRequest,他看起来是这个样子的:
然后,我们可以在YourBaseRequest的init方法中做一些事情,比如,设置host(baseUrl):
self.host = @"https://api.lsy.com";
添加公共请求参数:
[self addExtraParamsWithDictionary:@{
@"userId":@"11111111111"
}];
添加请求头:
self.httpRequestHeaders = @{
@"version":@"1.0.0",
@"platform":@"iOS"
};
这样,一些基本的功能就有了。
创建你的BaseResponse
有了BaseRequest,我们还需要有个BaseResponse,BaseResponse需要实现LSYResponseProtocol协议,这个协议大概是这个样子的:
服务器的返回值结构,也许每个公司的都不一样,但基本上都能划分成三个部分,即,code,message,result。
code代表状态码(错误码),不同的值代表着不同的含义,一般200代表请求成功。
message代表错误信息。
result代表返回的数据,可以是用户信息,商品列表等。
假设我们创建了YourBaseResponse,他看起来是这个样子的:
然后,我们需要给YourBaseResponse添加一个用服务器返回的response作为参数的初始化方法,在这个方法里,我们需要将返回的response中的内容,对应到YourBaseResponse中的code,message,result这三个字段。
假设你请求回来的数据结构是这样的:
{
"resultcode" = 200,
"resultmsg" = "请求成功!",
"result" = {
"name" = "xxx",
"sex" = 1,
"age" = 20
}
}
那么你的初始化方法应该是这样的:
然后,你需要实现LSYResponseProtocol中的isRequestSuccess方法,你需要告诉我,你希望code的值为多少代表请求成功,假设200代表成功,那么这个方法的实现大概是这个样子的:
接下来是json转自定义模型的相关协议的实现,这里有三个方法,分别是:
//将json转化为指定的model类的实例
- (instancetype)modelWithClass:(Class)resultClass json:(id)resultJson;
//将json转化为指定的model类实例的数组
- (NSArray *)modelArrayWithClass:(Class)elementClass json:(id)resultJson;
//将json转化为指定的model类实例的字典
- (NSDictionary *)modelDictionaryWithClass:(Class)elementClass json:(id)resultJson;
这样设计是为了解除LSYNetworking对模型转化库的耦合,你可以根据自己项目里的json转model的库进行实现,这个库可以是YYModel,Mantle,MJExtension等。
这里假设你用的是YYModel,那么实现看上去是这个样的:
- (instancetype)modelWithClass:(Class)resultClass json:(id)resultJson{
if ([resultClass isSubclassOfClass:NSDictionary.class] ||
[resultClass isSubclassOfClass:NSArray.class] ||
[resultClass isSubclassOfClass:NSString.class] ||
[resultClass isSubclassOfClass:NSNumber.class]) {
//如果resultClass是以上四种类型,则不需要转model,只有自定义模型需要转化
return resultJson;
}
return [resultClass yy_modelWithJSON:resultJson];
}
- (NSArray *)modelArrayWithClass:(Class)elementClass json:(id)resultJson{
//这里可以断言下类型是否匹配,或者可以做一下判断,如果不匹配,则直接返回resultJson/返回空
NSAssert([resultJson isKindOfClass:NSArray.class], @"Result class error,result json object is not a kind of NSArray!");
return [NSArray yy_modelArrayWithClass:elementClass json:resultJson];
}
- (NSDictionary *)modelDictionaryWithClass:(Class)elementClass json:(id)resultJson{
//这里可以断言下类型是否匹配,或者可以做一下判断,如果不匹配,则直接返回resultJson/返回空
NSAssert([resultJson isKindOfClass:NSDictionary.class], @"Result class error,result json object is not a kind of NSDictionary!");
return [NSDictionary yy_modelDictionaryWithClass:elementClass json:resultJson];
}
实现比较简单,直接用YYModel进行转化就行,这里在转化之前进行了类型判断,对特殊情况进行了容错处理。
到此为止,YourBaseResponse的工作就完成了,现在,我们需要将YourBaseResponse和YourBaseRequest进行绑定,我们需要在YourBaseRequest里重写handleResponse:方法,来处理服务器返回的数据,将这个数据,转化为YourBaseResponse的实例:
- (id<LSYResponseProtocol>)handleResponse:(id)response{
return [[YourBaseResponse alloc] initWithResponse:response];
}
至此,准备工作都已完成,接下来就可以开始发起一个网络请求了。
多个Host和多种Response结构的复杂情况
小公司一般只有一个Host,一种Response结构,那么定义一个BaseRequest和BaseResponse就够用了。不过很多大厂,都会遇到一个项目里有多个Host的情况,甚至是,不同的Host的Response结构也不一样。
遇到这样的复杂情况,我们可以在方法中进行判断,根据判断结果选择正确的Host,使用正确的Response字段。
当然,如果Host不太多的话,也可以将一些单独的逻辑抽出来,再写一个XXXModuleBaseRequest继承自YourBaseRequest,如下图所示:
如果Host太多了,就舍弃设置Host + apiName的模式吧,直接将Host设置为整个请求的url也是没问题的(我目前负责的项目里就有几十种Host,不过好在返回值的结构都是一样的,所以不需要特殊处理);
创建一个网络请求实例
假设,这里有个请求,用好友的userId请求好友的个人基本信息,接口地址为https://api.lsy.com/user/firend_info,请求参数为friendId,请求方式为POST,返回值为个人信息的字典,大致结构如下:
{
"resultcode" = 200,
"resultmsg" = "请求成功!",
"result" = {
"user_id" = 11111111111,
"name" = "xxx",
"sex" = 1,
"age" = 20
}
}
我们先创建一个XXXFriendInfoRequest,继承自YourBaseRequest,然后给这个类添加一个属性friendId:
@interface XXXFriendInfoRequest : YourBaseRequest
@property (assign, nonatomic) int friendId;
@end
接着,在初始化方法中设置请求的api地址
self.apiName = @"/user/firend_info";
当然,除了用属性的方式定义请求参数,你还可以用下边的方法添加一些额外的参数:
[self addExtraParamsWithDictionary:@{
@"osVersion":@"iOS 14.0.1"
}];
我们一般会用addExtraParamsWithDictionary:添加一些不需要外部传递的参数,或者是一些写死的参数。
默认请求方式是POST,如果需要GET请求,则需要将usePost属性设置为NO。
接下来,我们创建一个映射的模型XXXFirendInfo:
@interface XXXFirendInfo : NSObject<YYModel>
@property (assign, nonatomic) int userId;
@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic) int sex;
@property (assign, nonatomic) int age;
@end
因为返回的用户id的字段是user_id,而我们定义的属性是userId,所以我们需要用YYModel映射一下:
+(NSDictionary<NSString *,id> *)modelCustomPropertyMapper{
return @{@"userId":@"user_id"};
}
模型定义好了,我们需要将XXXFirendInfo设置为XXXFriendInfoRequest的result类型,即,让返回值json自动转化为XXXFirendInfo:
-(Class)resultClass{
return XXXFirendInfo.class;
}
如果不设置,则默认返回json。
如果服务器返回的是数组或字典,需要转化成model的容器,则resultClass需要返回对应的类型,同时要实现elementClassIfResultIsCollection方法,返回容器里的元素的类,进行转化:
-(Class)resultClass{
return NSArray.class;
}
-(Class)elementClassIfResultIsCollection{
return XXXFirendInfo.class;
}
这样,返回的数据就会被转化成XXXFirendInfo的数组,至此,网络请求实例创建完毕。
发起一次网络请求
在需要请求的地方,创建一个XXXFriendInfoRequest的实例,设置friendId的值,然后调用startRequestWithSuccessBlock:failureBlock:方法进行网络请求:
XXXFriendInfoRequest *request = [[XXXFriendInfoRequest alloc] init];
request.friendId = 11111111;
[request startRequestWithSuccessBlock:^(XXXFirendInfo *responseData) {
NSLog(@"Request success:%@",responseData);
} failureBlock:^(NSError * _Nonnull error) {
NSLog(@"Request failed:%@",error);
}];
Request中定义的属性,会自动转换为请求参数,所以不需要额外的处理。
这样,一次请求就完成了,怎么样,是不是很简单!
发起一次Multipart请求
在开发过程中,我们有时候会需要Multipart请求,比如上传个人信息的时候可能会上传个头像,发朋友圈的时候会上传一些图片等。LSYNetworking将Multipart请求传输的内容抽象成了LSYRequestUploadItem,根据传输文件的不同类型,选择不同的子类进行初始化,赋值,请求。
我们模拟一下微信发朋友圈的请求,假设请求的类是WXTimelinePublishRequest,有个参数content,代表文字内容,图片最多上传9张,上传的图片对应的key是image0-image8,那么代码大致如下:
WXTimelinePublishRequest *request = [[XXXFriendInfoRequest alloc] init];
request.content = @"刚才我看到了一个UFO!";
LSYRequestUploadImage *imageItem = [[LSYRequestUploadImage alloc] init];
imageItem.key = @"image0";
imageItem.image = [[UIImage alloc] init];
[request uploadFileWithItems:@[imageItem] progressBlock:nil successBlock:^(id responseData) {
NSLog(@"Request success:%@",responseData);
} failureBlock:^(NSError *error) {
NSLog(@"Request failed:%@",error);
}];
请求失败类型判断
我们知道,网络请求失败的情况分为两种,一种是HTTP的错误,一种是业务逻辑的错误。
HTTP错误是系统级别的错误,我们最熟悉的就是404(Not found)。
业务逻辑错误是指,请求其实已经通了,但是由于本次请求不符合一些业务上的逻辑,所以不能返回正确的数据,比如请求参数拼错了,比如userId不存在,比如用在未登录状态下请求了需要登录的接口。
HTTP的错误,一般只需要进行统一的toast弹窗提示,如"请求失败,请重试"。
而业务上的请求错误,一般需要客户端进行特殊的业务处理,比如用户需要在服务器返回未登录错误的时候调起登录逻辑。
LSYNetworking对失败情况进行了统一的回调,即不管是HTTP错误,还是业务逻辑错误,都会在failureBlock中进行回调,这样的好处是,避免了使用者需要在success回调中,还需要判断是否存在业务逻辑错误,而业务逻辑的错误,一般只需要在BaseRequest里进行公共的处理。
那么如何区分这两种错误呢?LSYNetworking给NSError写了个Category增加了一些方法,我们可以通过isBusinessError方法来判断是否是业务逻辑错误,可以用extraInfo来获取服务器返回的额外信息(如果有的话),即Response中的result部分的内容,大致代码如下:
if (error.isBusinessError) {
if (error.code == 302302) {
//获取后台返回的其他信息进行处理
NSDictionary *extraInfo = error.extraInfo;
}
}
请求参数定制
既然定义的属性会自动转化为请求参数,那么我们如何控制哪些属性转化,哪些属性不要转化?如何控制只转化当前请求的参数,不要转化父类的参数?
LSYNetworking给NSObject写了个Category,实现了将obj中的属性转换为NSDictionary的功能,并且定义了LSYPropertysToDictionaryProtocol代理协议,用来控制转化后的字典内容。而LSYBaseRequest遵守了这个协议。
LSYNetworking默认会将当前类的属性进行转化,如果需要将父类,以及更之前的父类的属性也进行转化,则需要实现协议中的propertysDictionaryUntilClass方法,返回一个父类的Class,表示从当前的类开始,截止到返回的父类为止,比如,如果希望将XXXFriendInfoRequest的父类属性也作为请求参数,则需要这样写:
- (Class)propertysDictionaryUntilClass{
//除了本类中定义的属性friendId,父类中的所有属性也要作为key-value添加到请求参数的dictionary当中
return self.superclass;
}
如果后台定义的请求参数不符合iOS的风格,则可以对参数进行映射,比如,服务器定义的请求参数为friend_id,但我们知道iOS属性的命名风格是驼峰法,所以对应的属性名称我们希望是friendId,那么我们就需要对参数进行映射,这里的思路和YYModel的modelCustomPropertyMapper相似:
+ (NSDictionary *)propertysToDictionaryMapper{
//属性friendId映射到请求参数的key为friend_id
return @{@"friendId":@"friend_id"};
}
这样,属性friendId转换后的参数字典的key就是friend_id了。
可以增加父类的属性作为参数,自然就可以将某些属性从转换中剔除,如果不希望某些属性转化为请求参数,可以将这些属性添加到黑名单中,所有黑名单中的属性不参与转化:
+ (nullable NSArray<NSString *> *)propertyBlacklist{
//这里表示,errorType这个属性不作为请求参数添加到参数dictionary中
NSMutableArray *blackList = @[@"errorType"].mutableCopy;
[blackList addObjectsFromArray:[super propertyBlacklist]];
return blackList;
}
发起一次下载/上传请求
下载请求请使用LSYDownloadRequest,该类实现了断点续传功能。
上传请求请使用LSYUploadRequest,这个类只是简单对AFN的上传进行了封装。
这两个功能使用起来比较简单,不再过多说明。
小结
基础篇就到此结束了,下一篇是进阶篇。
更多详细的内容,我写在了Github的 Demo里,有兴趣的可以下载下来看看。