前言
關(guān)于網(wǎng)絡(luò)層的設(shè)計,最主要的是和業(yè)務(wù)層的對接問題杖们。
網(wǎng)絡(luò)層設(shè)計得好悉抵,可以讓業(yè)務(wù)層開發(fā)事半功倍肩狂;反之摘完,若網(wǎng)絡(luò)層設(shè)計地很糟糕,則會讓業(yè)務(wù)層開發(fā)事倍功半傻谁,心里法克連連孝治。
關(guān)于網(wǎng)絡(luò)層和業(yè)務(wù)層的對接,我們一般從下面幾個方面進行考量:
- 選擇哪種方式請求網(wǎng)絡(luò)數(shù)據(jù)审磁,系統(tǒng)自帶的還是AFNetworking谈飒?
- 以什么模式給業(yè)務(wù)層交付數(shù)據(jù)?delegate 還是block态蒂?
- 交付給業(yè)務(wù)層什么形式的數(shù)據(jù)杭措?直接返回dict就行了,還是把dict在網(wǎng)絡(luò)層轉(zhuǎn)換為ResultModel再交給業(yè)務(wù)層钾恢?
- 封裝API應(yīng)該選擇集約型還是離散型手素?
第一個問題,“選擇哪種方式請求網(wǎng)絡(luò)數(shù)據(jù)瘩蚪?”泉懦。第三方庫AFNetworking很強大而且使用起來比較簡單,所以一般我們選擇AFNetworking疹瘦。蘋果自帶的NSURLSession等以后再研究崩哩。
第二個問題,“以什么模式給業(yè)務(wù)層交付數(shù)據(jù)言沐?”邓嘹。一般選擇Delegate和block。關(guān)于它們险胰,應(yīng)該說各有利弊吧汹押,具體使用場景具體選擇使用。block使用起來較方便鸯乃,但也有調(diào)試時不好追蹤鲸阻,容易出現(xiàn)循環(huán)引用等坑的缺點跋涣。而且若在業(yè)務(wù)層block返回數(shù)據(jù)后,要做比較復(fù)雜的邏輯處理的話鸟悴,那在block里會寫有大段代碼陈辱,這樣閱讀起來也不好,使代碼整體結(jié)構(gòu)顯得很不清晰细诸。
但是沛贪,在此,我們?nèi)韵纫詁lock為例來理解網(wǎng)絡(luò)層的設(shè)計震贵。
第三個問題利赋,“交付給業(yè)務(wù)層什么形式的數(shù)據(jù)?”猩系。我們設(shè)計網(wǎng)絡(luò)層媚送,就要想著能盡量減輕業(yè)務(wù)層的開發(fā)量,最好把網(wǎng)絡(luò)層從后臺拿到的一大串?dāng)?shù)據(jù)寇甸,剝離塘偎、加工、整理成業(yè)務(wù)層需要的數(shù)據(jù)格式然后再交付給它拿霉。
第四個問題吟秩,“封裝API應(yīng)該選擇集約型還是離散型?”绽淘。所謂集約型涵防,就是只能業(yè)務(wù)層提供一個方法,所有業(yè)務(wù)層的網(wǎng)絡(luò)請求都要通過該方法完成沪铭。因此壮池,該方法至少要能傳入接口路徑(path)、請求方式(get/post)伦意、請求參數(shù)(param)等火窒。集約型的好處是對于網(wǎng)絡(luò)層的編寫來說方便快捷,但對業(yè)務(wù)層來說要傳入這么多參數(shù)并不太好驮肉。我們設(shè)計的目的就是盡量使業(yè)務(wù)層使用起來簡單輕巧熏矿,所以我們常常采用離散型方式。(說得不太恰當(dāng)离钝。集約型為所有的業(yè)務(wù)請求提供一個接口票编,省去了編寫業(yè)務(wù)模塊xxxManager的工作量。但對集約型而言卵渴,提供的這個唯一的網(wǎng)絡(luò)請求方法得有接口地址慧域,請求方式,接口參數(shù)等多個參數(shù)浪读。這是其繁瑣之處昔榴,而離散型則為了避免給業(yè)務(wù)層帶來這樣的繁瑣辛藻,而在xxxManager提供的接口方法里自己配置了接口地址interface
和請求方式,并以方法名加以體現(xiàn)互订。那對業(yè)務(wù)層開發(fā)來說就簡潔明了了許多吱肌。一個比如用戶模塊UserManager里的對登錄請求的封裝,只需業(yè)務(wù)層傳入account
和password
兩個參數(shù)仰禽,而接口地址和請求方式已封裝在其方法里了login:password:success:failure
氮墨,而且方法名也體現(xiàn)出了請求接口login
。但離散型的問題是無疑為增加代碼量吐葵,為編寫xxxManager層將花費大量時間规揪。)
不言而喻,和集約型相對的温峭,離散型就是根據(jù)功能模塊分為不同的模塊猛铅,分別提供不同的方法給業(yè)務(wù)層調(diào)用。比如诚镰,把和用戶有關(guān)的所有網(wǎng)絡(luò)請求奕坟,放在一個叫UserManager的類中,登錄清笨、注冊、修改密碼等分別提供不同的方法刃跛,這樣的好處在于抠艾,一、不同功能模塊放在不同的文件中桨昙,使項目結(jié)構(gòu)更清晰检号,維護升級更容易;二蛙酪、對于業(yè)務(wù)開發(fā)人員來說齐苛,不同的功能叫不同的方法名這樣更友好易懂。三桂塞、更重要的是凹蜂,你可以在xxxManager這一層做一些針對該模塊的個性化處理。沒錯阁危,你可以在這一層完成上個問題中所說的數(shù)據(jù)加工后再交付給業(yè)務(wù)層玛痊。我們把和用戶相關(guān)的網(wǎng)絡(luò)請求API都定義在UserManager類中,并在其中轉(zhuǎn)換為UserObject然后交付給業(yè)務(wù)層狂打。
除此外擂煞,“離散”不僅體現(xiàn)在提供的API方法上,還體現(xiàn)在網(wǎng)絡(luò)請求連接上趴乡。我們定義一個HttpClient類对省,在該類中專門完成對服務(wù)器的網(wǎng)絡(luò)請求蝗拿。并且給xxxManager這一層提供不同請求方式對應(yīng)的方法。
好了蒿涎,基本結(jié)構(gòu)就是這樣蛹磺,下面上代碼。我們“從內(nèi)至外”的看代碼同仆。
首先就是HttpClient這個類了萤捆,該類是完成網(wǎng)絡(luò)請求連接的核心。并給xxxManager提供網(wǎng)絡(luò)連接的接口方法俗批。
HttpClient.h
#import <Foundation/Foundation.h>
#import "AFNetworking.h"
#define BaseURL @"http://192.168.1.125/v1/" // 服務(wù)器地址
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請求對應(yīng)的兩個方法調(diào)用,但其實在內(nèi)部岁忘,我們是定義了一個“全能方法”來完成網(wǎng)絡(luò)連接的辛慰,這才是核心。
#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];
}
// 完成網(wǎng)絡(luò)連接的核心
- (void)sendRequestOfType:(RequestMethod)requestType
path:(NSString *)path
prama:(id)prama
success:(void(^)(id result))success
failure:(void(^)(NSError *error))failure
{
// 拼接參數(shù)干像,得到完整的接口地址
NSURL *baseUrl = [NSURL URLWithString:BaseURL];
NSString *pathUrl = [NSString stringWithFormat:@"%@%@",BaseURL,path];
/**
通常在此帅腌,可以完成拼接公共參數(shù)、密碼加密麻汰、或者簽名認證等操作速客。
*/
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
好了,現(xiàn)在看看xxxManager層五鲫。
UserManager.h
#import <Foundation/Foundation.h>
#import "UserObject.h"
@interface UserManager : NSObject
// 提供獲取UserManager實例的類方法
+ (UserManager *)getInstance;
// 給業(yè)務(wù)層提供的“登錄”功能的網(wǎng)絡(luò)數(shù)據(jù)請求方法
- (void)login:(NSString *)account
password:(NSString *)password
success:(void(^)(UserObject *userObj))success
failure:(void(^)(NSError *error))failure;
@end
UserManager.m
登錄溺职、注冊、修改密碼位喂、修改個人資料分別提供不同的方法浪耘。在相應(yīng)方法里通過調(diào)用HttpClient提供的網(wǎng)絡(luò)連接方法完成網(wǎng)絡(luò)連接,然后把數(shù)據(jù)轉(zhuǎn)換加工成業(yè)務(wù)層需要的數(shù)據(jù)格式UserObject塑崖,再交付之七冲。
#import "UserManager.h"
#import "HttpClient.h"
#include "MJExtension.h"
@implementation UserManager
//=================================== UserManager ==========================================//
// 和User有關(guān)的所有請求接口路徑
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提供的請求方法完成網(wǎng)絡(luò)請求
HttpClient *httpClient = [HttpClient new];
[httpClient postOfPath:kUserLogin prama:pramaDict success:^(id result) {
// 把服務(wù)器返回的json數(shù)據(jù)result轉(zhuǎn)換為UserObject類型的userObj
UserObject *userObj = [UserObject mj_objectWithKeyValues:result];
success(userObj);
} failure:^(NSError *error) {
failure(error);
}];
}
@end
好了。當(dāng)業(yè)務(wù)層開發(fā)人員需要完成“登錄”功能時规婆,只需調(diào)用UserManager中我們定義的login方法就得到了網(wǎng)絡(luò)數(shù)據(jù)澜躺,并且已轉(zhuǎn)為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
補充和優(yōu)化
上面我們實現(xiàn)了一個簡單的網(wǎng)絡(luò)層聋呢,但其實是比較簡陋的苗踪。真是情況要考慮很多地方的。
1. 在請求中添加簽名認證削锰,保證請求來源于我們自己的APP通铲。
2. 取消無用的請求。
比如器贩,比如我們剛進入一個界面后颅夺,此刻便會發(fā)出一條該界面數(shù)據(jù)的請求朋截,但是此時用戶卻點了“返回”,退回了上個界面吧黄。此時上個界面的請求已經(jīng)飛出但還未完成部服。這時,我們應(yīng)當(dāng)取消上個界面的請求拗慨,釋放帶寬廓八。這樣對于下來的網(wǎng)絡(luò)請求是有利的。
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請求剧蹂,請求成功時回調(diào)block返回responseObject,失敗時返回error烦却。但是請注意宠叼,這里的錯誤回調(diào)僅僅指網(wǎng)絡(luò)請求錯誤,要注意區(qū)分網(wǎng)絡(luò)錯誤和業(yè)務(wù)錯誤(也就是網(wǎng)絡(luò)請求是成功的其爵,但是對于我們的業(yè)務(wù)來說冒冬,是有問題的)。這些信息同樣是會在responseObject返回摩渺。實際上一般網(wǎng)絡(luò)請求成功后简烤,后臺返回的responseObject一般都有errorCode字段,只有當(dāng)errorCode=0時证逻,就說明一切OK乐埠,正常返回了我們需要的數(shù)據(jù)。所以囚企,為了給業(yè)務(wù)層提供方便,我們還得在網(wǎng)絡(luò)層做些處理瑞眼。使交付給業(yè)務(wù)層的數(shù)據(jù)里龙宏,成功回調(diào)的block里就純粹了業(yè)務(wù)邏輯意義上正確的數(shù)據(jù),而失敗的回調(diào)里的數(shù)據(jù)則包括一切錯誤信息伤疙。所說的處理就是在該方法里對回調(diào)block做層包裝银酗。
// 完成網(wǎng)絡(luò)連接的核心
- (void)sendRequestOfType:(RequestMethod)requestType
path:(NSString *)path
prama:(id)prama
success:(void(^)(id result))success
failure:(void(^)(NSError *error))failure
{
// 拼接參數(shù),得到完整的接口地址
NSURL *baseUrl = [NSURL URLWithString:BaseURL];
NSString *pathUrl = [NSString stringWithFormat:@"%@%@",BaseURL,path];
/**
通常在此徒像,可以拼接一些公共參數(shù)黍特,或者簽名認證參數(shù)。
*/
AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseUrl];
// ------------------ 包裝回調(diào)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); //業(yè)務(wù)邏輯意義上的正確返回锯蛀。
}
else
{
// 有錯誤灭衷。
NSError *error = [NSError errorWithDomain:result.message code:result.errorCode userInfo:nil];
failure(error);
}
}
else
{
NSError *error = [NSError errorWithDomain:@"服務(wù)返回數(shù)據(jù)異常" 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.多服務(wù)器多環(huán)境切換:**
一般比較規(guī)范的項目都有開發(fā)環(huán)境、測試環(huán)境旁涤、預(yù)發(fā)布環(huán)境翔曲、正式環(huán)境(生產(chǎn)環(huán)境)四種環(huán)境迫像,它們對應(yīng)的服務(wù)器地址分別是不同的。在項目版本迭代過程以“開發(fā)——>測試——>預(yù)發(fā)布——>正式”這個順序進行的瞳遍。開發(fā)環(huán)境就是新需求下來后的更改闻妓。測試環(huán)境就是給測試打了包后,改bug時的更改掠械。預(yù)發(fā)布環(huán)境就是測試基本完成由缆,交付給運營測試,改動基本比較小猾蒂。正式環(huán)境不用解釋均唉,不言而喻。
我們可以把多環(huán)境的配置寫在預(yù)編譯頭文件中:
/************環(huán)境配置開關(guān)**********
* OPEN_TEST 0:為開發(fā)環(huán)境
* 1:為測試環(huán)境
* 2:為預(yù)發(fā)布外網(wǎng)環(huán)境
* 其他:為生產(chǎn)環(huán)境
***************************/
#define OPEN_TEST 0
#if (OPEN_TEST == 0)/************開發(fā)環(huán)境************/
#define HTTPSURLEVER @"http://www.runedu.test/api"
#elif (OPEN_TEST == 1)/************測試環(huán)境************/
#define HTTPSURLEVER @"http://www.rjy.rd/api"
#elif (OPEN_TEST == 2)/************預(yù)發(fā)布環(huán)境************/
#define HTTPSURLEVER @"http://www.prerjy.com/api"
#else/************生產(chǎn)環(huán)境************/
#define HTTPSURLEVER @"http://www.runjiaoyu.com.cn/api"
#endif