前言
关于网络层的设计,最主要的是和业务层的对接问题。
网络层设计得好,可以让业务层开发事半功倍;反之,若网络层设计地很糟糕,则会让业务层开发事倍功半,心里法克连连。
关于网络层和业务层的对接,我们一般从下面几个方面进行考量:
- 选择哪种方式请求网络数据,系统自带的还是AFNetworking?
- 以什么模式给业务层交付数据?delegate 还是block?
- 交付给业务层什么形式的数据?直接返回dict就行了,还是把dict在网络层转换为ResultModel再交给业务层?
- 封装API应该选择集约型还是离散型?
第一个问题,“选择哪种方式请求网络数据?”。第三方库AFNetworking很强大而且使用起来比较简单,所以一般我们选择AFNetworking。苹果自带的NSURLSession等以后再研究。
第二个问题,“以什么模式给业务层交付数据?”。一般选择Delegate和block。关于它们,应该说各有利弊吧,具体使用场景具体选择使用。block使用起来较方便,但也有调试时不好追踪,容易出现循环引用等坑的缺点。而且若在业务层block返回数据后,要做比较复杂的逻辑处理的话,那在block里会写有大段代码,这样阅读起来也不好,使代码整体结构显得很不清晰。
但是,在此,我们仍先以block为例来理解网络层的设计。
第三个问题,“交付给业务层什么形式的数据?”。我们设计网络层,就要想着能尽量减轻业务层的开发量,最好把网络层从后台拿到的一大串数据,剥离、加工、整理成业务层需要的数据格式然后再交付给它。
第四个问题,“封装API应该选择集约型还是离散型?”。所谓集约型,就是只能业务层提供一个方法,所有业务层的网络请求都要通过该方法完成。因此,该方法至少要能传入接口路径(path)、请求方式(get/post)、请求参数(param)等。集约型的好处是对于网络层的编写来说方便快捷,但对业务层来说要传入这么多参数并不太好。我们设计的目的就是尽量使业务层使用起来简单轻巧,所以我们常常采用离散型方式。(说得不太恰当。集约型为所有的业务请求提供一个接口,省去了编写业务模块xxxManager的工作量。但对集约型而言,提供的这个唯一的网络请求方法得有接口地址,请求方式,接口参数等多个参数。这是其繁琐之处,而离散型则为了避免给业务层带来这样的繁琐,而在xxxManager提供的接口方法里自己配置了接口地址interface
和请求方式,并以方法名加以体现。那对业务层开发来说就简洁明了了许多。一个比如用户模块UserManager里的对登录请求的封装,只需业务层传入account
和password
两个参数,而接口地址和请求方式已封装在其方法里了login:password:success:failure
,而且方法名也体现出了请求接口login
。但离散型的问题是无疑为增加代码量,为编写xxxManager层将花费大量时间。)
不言而喻,和集约型相对的,离散型就是根据功能模块分为不同的模块,分别提供不同的方法给业务层调用。比如,把和用户有关的所有网络请求,放在一个叫UserManager的类中,登录、注册、修改密码等分别提供不同的方法,这样的好处在于,一、不同功能模块放在不同的文件中,使项目结构更清晰,维护升级更容易;二、对于业务开发人员来说,不同的功能叫不同的方法名这样更友好易懂。三、更重要的是,你可以在xxxManager这一层做一些针对该模块的个性化处理。没错,你可以在这一层完成上个问题中所说的数据加工后再交付给业务层。我们把和用户相关的网络请求API都定义在UserManager类中,并在其中转换为UserObject然后交付给业务层。
除此外,“离散”不仅体现在提供的API方法上,还体现在网络请求连接上。我们定义一个HttpClient类,在该类中专门完成对服务器的网络请求。并且给xxxManager这一层提供不同请求方式对应的方法。
好了,基本结构就是这样,下面上代码。我们“从内至外”的看代码。
首先就是HttpClient这个类了,该类是完成网络请求连接的核心。并给xxxManager提供网络连接的接口方法。
HttpClient.h
#import <Foundation/Foundation.h>
#import "AFNetworking.h"
#define BaseURL @"http://192.168.1.125/v1/" // 服务器地址
typedef NS_ENUM(NSInteger, RequestMethod)
{
POST = 0,
GET,
PUT,
DELETE,
};
@interface HttpClient : NSObject
// get请求
- (void)getOfPath:(NSString *)path
prama:(id)prama
success:(void(^)(id result))success
failure:(void(^)(NSError *error))failure;
// post请求
- (void)postOfPath:(NSString *)path
prama:(id)prama
success:(void(^)(id result))success
failure:(void(^)(NSError *error))failure;
@end
HttpClient.m
注意,我们提供给外部get和post请求对应的两个方法调用,但其实在内部,我们是定义了一个“全能方法”来完成网络连接的,这才是核心。
#import "HttpClient.h"
@implementation HttpClient
// get
- (void)getOfPath:(NSString *)path
prama:(id)prama
success:(void(^)(id result))success
failure:(void(^)(NSError *error))failure
{
[self sendRequestOfType:GET path:path prama:prama success:success failure:failure];
}
// post
- (void)postOfPath:(NSString *)path
prama:(id)prama
success:(void(^)(id result))success
failure:(void(^)(NSError *error))failure
{
[self sendRequestOfType:POST path:path prama:prama success:success failure:failure];
}
// 完成网络连接的核心
- (void)sendRequestOfType:(RequestMethod)requestType
path:(NSString *)path
prama:(id)prama
success:(void(^)(id result))success
failure:(void(^)(NSError *error))failure
{
// 拼接参数,得到完整的接口地址
NSURL *baseUrl = [NSURL URLWithString:BaseURL];
NSString *pathUrl = [NSString stringWithFormat:@"%@%@",BaseURL,path];
/**
通常在此,可以完成拼接公共参数、密码加密、或者签名认证等操作。
*/
AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseUrl];
switch (requestType)
{
// ------------ GET -------------
case GET:
{
[manager GET:pathUrl
parameters:prama
success:^(AFHTTPRequestOperation *operation, id responseObject) {
success(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
failure(error);
}];
break;
}
// ------------ POST -------------
case POST:
{
[manager POST:pathUrl
parameters:prama success:^(AFHTTPRequestOperation *operation, id responseObject) {
success(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
failure(error);
}];
break;
}
default:
break;
}
}
@end
好了,现在看看xxxManager层。
UserManager.h
#import <Foundation/Foundation.h>
#import "UserObject.h"
@interface UserManager : NSObject
// 提供获取UserManager实例的类方法
+ (UserManager *)getInstance;
// 给业务层提供的“登录”功能的网络数据请求方法
- (void)login:(NSString *)account
password:(NSString *)password
success:(void(^)(UserObject *userObj))success
failure:(void(^)(NSError *error))failure;
@end
UserManager.m
登录、注册、修改密码、修改个人资料分别提供不同的方法。在相应方法里通过调用HttpClient提供的网络连接方法完成网络连接,然后把数据转换加工成业务层需要的数据格式UserObject,再交付之。
#import "UserManager.h"
#import "HttpClient.h"
#include "MJExtension.h"
@implementation UserManager
//=================================== UserManager ==========================================//
// 和User有关的所有请求接口路径
NSString *const kUserLogin = @"user/login";
NSString *const kUserRegister = @"user/register";
+ (UserManager *)getInstance
{
return [UserManager new];
}
- (void)login:(NSString *)account
password:(NSString *)password
success:(void(^)(UserObject *userObj))success
failure:(void(^)(NSError *error))failure
{
NSDictionary *pramaDict = @{@"account":account, @"password":password};
// 通过HttpClient提供的请求方法完成网络请求
HttpClient *httpClient = [HttpClient new];
[httpClient postOfPath:kUserLogin prama:pramaDict success:^(id result) {
// 把服务器返回的json数据result转换为UserObject类型的userObj
UserObject *userObj = [UserObject mj_objectWithKeyValues:result];
success(userObj);
} failure:^(NSError *error) {
failure(error);
}];
}
@end
好了。当业务层开发人员需要完成“登录”功能时,只需调用UserManager中我们定义的login方法就得到了网络数据,并且已转为UserObject给我们。
#import "ViewController.h"
#import "UserManager.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[[UserManager getInstance] login:@"wang66" password:@"123456" success:^(UserObject *userObj) {
NSLog(@"登录后后台返回用户信息----%@",userObj.description);
} failure:^(NSError *error) {
NSLog(@"登录失败----%@",error);
}];
}
@end
补充和优化
上面我们实现了一个简单的网络层,但其实是比较简陋的。真是情况要考虑很多地方的。
1. 在请求中添加签名认证,保证请求来源于我们自己的APP。
2. 取消无用的请求。
比如,比如我们刚进入一个界面后,此刻便会发出一条该界面数据的请求,但是此时用户却点了“返回”,退回了上个界面。此时上个界面的请求已经飞出但还未完成。这时,我们应当取消上个界面的请求,释放带宽。这样对于下来的网络请求是有利的。
3. 错误信息的处理
// ------------ GET -------------
case GET:
{
[manager GET:pathUrl
parameters:prama
success:^(AFHTTPRequestOperation *operation, id responseObject) {
success(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
failure(error);
}];
break;
}
这段代码,AFNetworking提供的GET请求,请求成功时回调block返回responseObject,失败时返回error。但是请注意,这里的错误回调仅仅指网络请求错误,要注意区分网络错误和业务错误(也就是网络请求是成功的,但是对于我们的业务来说,是有问题的)。这些信息同样是会在responseObject返回。实际上一般网络请求成功后,后台返回的responseObject一般都有errorCode字段,只有当errorCode=0时,就说明一切OK,正常返回了我们需要的数据。所以,为了给业务层提供方便,我们还得在网络层做些处理。使交付给业务层的数据里,成功回调的block里就纯粹了业务逻辑意义上正确的数据,而失败的回调里的数据则包括一切错误信息。所说的处理就是在该方法里对回调block做层包装。
// 完成网络连接的核心
- (void)sendRequestOfType:(RequestMethod)requestType
path:(NSString *)path
prama:(id)prama
success:(void(^)(id result))success
failure:(void(^)(NSError *error))failure
{
// 拼接参数,得到完整的接口地址
NSURL *baseUrl = [NSURL URLWithString:BaseURL];
NSString *pathUrl = [NSString stringWithFormat:@"%@%@",BaseURL,path];
/**
通常在此,可以拼接一些公共参数,或者签名认证参数。
*/
AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseUrl];
// ------------------ 包装回调block ------------------
//请求成功block
void(^ok)(id responseObject) = ^(id responseObject){
if([responseObject isKindOfClass:[NSDictionary class]])
{
Result *result = [Result mj_objectWithKeyValues:responseObject];
if (result.errorCode == 0)
{
success(responseObject); //业务逻辑意义上的正确返回。
}
else
{
// 有错误。
NSError *error = [NSError errorWithDomain:result.message code:result.errorCode userInfo:nil];
failure(error);
}
}
else
{
NSError *error = [NSError errorWithDomain:@"服务返回数据异常" code:-1 userInfo:nil];
failure(error);
}
};
//请求失败block
void(^fail)(NSError *error) = ^(NSError *error){
failure(error);
};
// ------------------------------------
switch (requestType)
{
// ------------ GET -------------
case GET:
{
[manager GET:pathUrl
parameters:prama
success:^(AFHTTPRequestOperation *operation, id responseObject) {
// success(responseObject);
ok(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// failure(error);
fail(error);
}];
break;
}
// ------------ POST -------------
case POST:
{
[manager POST:pathUrl
parameters:prama success:^(AFHTTPRequestOperation *operation, id responseObject) {
// success(responseObject);
ok(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// failure(error);
fail(error);
}];
break;
}
default:
break;
}
}
** 4.多服务器多环境切换:**
一般比较规范的项目都有开发环境、测试环境、预发布环境、正式环境(生产环境)四种环境,它们对应的服务器地址分别是不同的。在项目版本迭代过程以“开发——>测试——>预发布——>正式”这个顺序进行的。开发环境就是新需求下来后的更改。测试环境就是给测试打了包后,改bug时的更改。预发布环境就是测试基本完成,交付给运营测试,改动基本比较小。正式环境不用解释,不言而喻。
我们可以把多环境的配置写在预编译头文件中:
/************环境配置开关**********
* OPEN_TEST 0:为开发环境
* 1:为测试环境
* 2:为预发布外网环境
* 其他:为生产环境
***************************/
#define OPEN_TEST 0
#if (OPEN_TEST == 0)/************开发环境************/
#define HTTPSURLEVER @"http://www.runedu.test/api"
#elif (OPEN_TEST == 1)/************测试环境************/
#define HTTPSURLEVER @"http://www.rjy.rd/api"
#elif (OPEN_TEST == 2)/************预发布环境************/
#define HTTPSURLEVER @"http://www.prerjy.com/api"
#else/************生产环境************/
#define HTTPSURLEVER @"http://www.runjiaoyu.com.cn/api"
#endif