前言
關于網絡層的設計,最主要的是和業務層的對接問題。
網絡層設計得好,可以讓業務層開發事半功倍;反之,若網絡層設計地很糟糕,則會讓業務層開發事倍功半,心里法克連連。
關于網絡層和業務層的對接,我們一般從下面幾個方面進行考量:
- 選擇哪種方式請求網絡數據,系統自帶的還是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