给大家分享一下我封装的一个基于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里,有兴趣的可以下载下来看看。
网友评论