一、背景
目前,公司里的App基本上采用了MVVM+ReactiveCocoa的模式來(lái)開(kāi)發(fā)。所依賴的網(wǎng)絡(luò)層私有庫(kù)是DDNetWork。其內(nèi)部是通過(guò)工程里的DDNetWorkManager單例子來(lái)進(jìn)行調(diào)用,并且4xx、5xx類的錯(cuò)誤直接在網(wǎng)絡(luò)庫(kù)內(nèi)部彈出提示,導(dǎo)致視圖層對(duì)這部分的錯(cuò)誤信息不可控。并且每次在ViewModel里涉及到網(wǎng)絡(luò)請(qǐng)求的部分,都需要寫(xiě)如下的代碼:
- (RACSignal *)signal_waitDoneOrders
{
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
DDRequestInfo *request = [DDRequestInfo createDDStaffRequestTransporterWaitDoneOrderNum];
dispatch_async(dispatch_get_main_queue(), ^{
[[DDNetWorkManager defaultManger] sendGETRequest:request success:^(id response) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
if (response)
{
[subscriber sendNext:response];
[subscriber sendCompleted];
}
else
{
NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:errorCode userInfo:userInfo];
[subscriber sendError:error];
}
});
} andFalilur:^(NSError *error, NSInteger errCode) {
[subscriber sendError:error];
}];
});
});
return [RACDisposable disposableWithBlock:^{
// 需要cancel網(wǎng)絡(luò)請(qǐng)求。
}];
}];
return [[signal replayLazily] setNameWithFormat:@"-signalWaitDoneOrders:%@", @"red_point"];
}
- 一是網(wǎng)絡(luò)請(qǐng)求比較多的模塊,就會(huì)造成重復(fù)代碼的急劇增加。
- 另外一個(gè)是每創(chuàng)建一個(gè)新的請(qǐng)求,工程師就要去寫(xiě)這么長(zhǎng)的一段代碼,心累。
- 重新封裝一個(gè)網(wǎng)絡(luò)庫(kù),費(fèi)時(shí)費(fèi)力,也沒(méi)有必要。
于是,決定在原來(lái)網(wǎng)絡(luò)庫(kù)的基礎(chǔ)上,參照octokit.objc進(jìn)行網(wǎng)絡(luò)請(qǐng)求封裝的優(yōu)化,使其增加RAC的能力。
二、改造網(wǎng)絡(luò)庫(kù)
1、向外傳遞NSURLSessionDataTask。
這一步的目的是為了可以在發(fā)起網(wǎng)絡(luò)請(qǐng)求的對(duì)象里面方便地取消請(qǐng)求。即將上面代碼部分的:
return [RACDisposable disposableWithBlock:^{
// 需要cancel網(wǎng)絡(luò)請(qǐng)求。
}];
修改為:
return [RACDisposable disposableWithBlock:^{
// 需要cancel網(wǎng)絡(luò)請(qǐng)求。
if (task.state != NSURLSessionTaskStateCompleted) {
[task cancel];
}
}];
這樣,外部就可以通過(guò)這個(gè)RACDisposable對(duì)象來(lái)及時(shí)地取消對(duì)應(yīng)的請(qǐng)求。
所以DDNetwork的每一個(gè)消息由返回void修改為返回NSURLSessionDataTask,如GET請(qǐng)求:
- (void)sendGETRequest:(DDRequestInfo *)request HTTPHeader:(NSDictionary *)headerDictionary success:(successHandler)success andFalilur:(failurHandler)failur;
修改為:
- (NSURLSessionDataTask *)sendGETRequest:(DDRequestInfo *)request HTTPHeader:(NSDictionary *)headerDictionary success:(successHandler)success andFalilur:(failurHandler)failur;
2、向外傳遞NSHTTPURLResponse
在請(qǐng)求結(jié)束、獲取返回信息的時(shí)候??赏ㄟ^(guò)NSHTTPURLResponse的statusCode來(lái)決定是否處理返回的數(shù)據(jù)。比如statusCode為304的時(shí)候,subscriber可以直接調(diào)用sendCompleted結(jié)束。
那么DDNetwork消息的success參數(shù)數(shù)據(jù)類型successHandler:
typedef void (^successHandler)(id responseObject);
修改為:
typedef void (^successHandler)(NSHTTPURLResponse *response, id responseObject);
則GET請(qǐng)求的返回:
NSURLSessionDataTask *task = [manager GET:url parameters:request.requestParameters progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
if (success) {
success(responseObject);
}
}];
修改為:
NSURLSessionDataTask *task = [manager GET:url parameters:request.requestParameters progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
if (success) {
success((NSHTTPURLResponse *)task.response,responseObject);
}
}];
3、向外傳遞 NSError
第一步、將錯(cuò)誤分域。
因?yàn)锳PI返回的錯(cuò)誤code有可能也是4xx、5xx等,但是所表示的錯(cuò)誤信息和服務(wù)器的4xx、5xx等是不一樣的。為了區(qū)別對(duì)待。將所有API返回的錯(cuò)誤劃分為:DDAPIErrorDomain。其他情況產(chǎn)生的域保持不變。
DDNetWorkModel.h
extern NSString * const DDAPIErrorDomain;
DDNetWorkModel.m
NSString * const DDAPIErrorDomain = @"DDAPIErrorDomain";
然后在構(gòu)造NSError的時(shí)候:
- (NSError *)errorFromServerResponse:(id)responseObject
{
NSString *errMessage = stringFromObject(responseObject, @"errorMsg");
NSInteger errorCode = [stringFromObject(responseObject, @"errorCode") integerValue];
NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey:errMessage};
NSError *error = [NSError errorWithDomain:DDAPIErrorDomain code:errorCode userInfo:userInfo];
return error;
}
第二步、改造網(wǎng)絡(luò)請(qǐng)求的failure:^(NSURLSessionDataTask *task, NSError *error) {}
__weak typeof(self) wself = self;
NSURLSessionDataTask *task = [manager POST:url parameters:request.requestParameters progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
// 處理請(qǐng)求成功。
} failure:^(NSURLSessionDataTask *task, NSError *error) {
__strong typeof(wself) sself = wself;
[sself changeTerminateRequestModelWhenErrorWihtAddress:request.actionAddress andError:error];
if ([sself isShowErrorProcessInfoWithAddress:request.actionAddress] == YES)
{
NSString *errorMsg = [sself cuteMessageWithErrorCode:[sself errorCodeWithError:error]];
NSString *errMessage = errorMsg.length > 0 ? errorMsg : (error.localizedFailureReason.length > 0 ? error.localizedFailureReason:@"");
// 直接toas展示errMessage
}
else
{
if (failur) {
failur(nil, error.code);
}
}
}];
修改為:
__weak typeof(self) wself = self;
NSURLSessionDataTask *task = [manager POST:url parameters:request.requestParameters progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
// 處理請(qǐng)求成功。
} failure:^(NSURLSessionDataTask *task, NSError *error) {
__strong typeof(wself) sself = wself;
[sself changeTerminateRequestModelWhenErrorWihtAddress:request.actionAddress andError:error];
if ([sself isShowErrorProcessInfoWithAddress:request.actionAddress] == YES)
{
NSString *errorMsg = [sself cuteMessageWithErrorCode:[sself errorCodeWithError:error]];
NSString *errMessage = errorMsg.length > 0 ? errorMsg : (error.localizedFailureReason.length > 0 ? error.localizedFailureReason:@"");
// 向外傳遞到外部視圖展示。
NSError *tError = [NSError errorWithDomain:error.domain code:error.code userInfo:@{NSLocalizedFailureReasonErrorKey:errMessage}];
if (failur) {
failur(tError, tError.code);
}
}
else
{
if (failur) {
failur(nil, error.code);
}
}
}];
第三步、網(wǎng)絡(luò)不可達(dá),及時(shí)返回
如果網(wǎng)絡(luò)本身不可達(dá)。那么需要在請(qǐng)求發(fā)出去之前就返回錯(cuò)誤,通知上層發(fā)生網(wǎng)絡(luò)連接錯(cuò)誤。
首先、在DDNetWorkModel初始化的時(shí)候,開(kāi)啟網(wǎng)絡(luò)狀態(tài)的嗅探。
- (instancetype)init
{
if (self = [super init])
{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
self.httpSessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configuration];
// 開(kāi)始網(wǎng)絡(luò)狀態(tài)嗅探
[self.httpSessionManager.reachabilityManager startMonitoring];
// 其他處理
}
return self;
}
其次、在每個(gè)網(wǎng)絡(luò)請(qǐng)求的第一步,判斷網(wǎng)絡(luò)是否可達(dá)。
- (NSURLSessionDataTask *)sendGETRequest:(DDRequestInfo *)request
HTTPHeader:(NSDictionary *)headerDictionary
success:(successHandler)success
andFalilur:(failurHandler)failur
{
// 先判斷網(wǎng)絡(luò)是否ok。
if (self.httpSessionManager.reachabilityManager.networkReachabilityStatus == AFNetworkReachabilityStatusNotReachable)
{
NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"無(wú)法連接到網(wǎng)絡(luò)", @"")];
// 構(gòu)造連接失敗的error。
NSError *error = [self connectErrorWithFailureReason:failureReason];
if (failur) {
failur(error, error.code);
}
return nil;
}
// 創(chuàng)建 AFHTTPSessionManager 對(duì)象,以及發(fā)起 GET 請(qǐng)求。
NSURLSessionDataTask *task = [manager GET:url parameters:request.requestParameters progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
// xxxx
} failure:^(NSURLSessionDataTask *task, NSError *error) {
// xxxx
}];
return task;
}
因?yàn)檫@一步是客戶端主動(dòng)判斷網(wǎng)絡(luò)是否可達(dá)。定義其錯(cuò)誤域?yàn)椋?code>DDClientErrorDomain,錯(cuò)誤碼為:668。
DDNetWorkModel.h
extern NSString * const DDClientErrorDomain;
DDNetWorkModel.m
NSString * const DDClientErrorDomain = @"DDClientErrorDomain";
經(jīng)過(guò)以上的修改,成功地將網(wǎng)絡(luò)庫(kù)里的NSURLSessionDataTask、NSHTTPURLResponse以及錯(cuò)誤信息傳遞到了外部。并且網(wǎng)絡(luò)庫(kù)只提供網(wǎng)絡(luò)請(qǐng)求相關(guān)的功能,不再提供錯(cuò)誤信息的toast提示。
三、DDNetWorkManager增加RAC擴(kuò)展
@interface DDNetWorkManger (RAC)
- (RACSignal *)enqueueRequest:(DDRequestInfo *)request method:(DDHTTPMethod)method resultClass:(Class)resultClass;
@end
這里只提供一個(gè)對(duì)外的接口。將請(qǐng)求加入隊(duì)列中。
其中DDHTTPMethod目前支持三種請(qǐng)求類型:
typedef NS_ENUM(NSUInteger, DDHTTPMethod) {
DDHTTPMethodGET,
DDHTTPMethodPOST,
DDHTTPMethodPUT
};
resultClass參數(shù)用來(lái)支持將返回的json數(shù)據(jù)直接轉(zhuǎn)換為指定的數(shù)據(jù)模型(resultClass)。
目前,只提供基礎(chǔ)的result數(shù)據(jù)模型DDAPIResult:
@interface DDAPIResult : DDObject
@property (nonatomic, copy) NSString *status;
// 請(qǐng)求結(jié)果
@property (nonatomic, copy) id content;
@property (nonatomic, copy) NSString *errorCode;
@property (nonatomic, copy) NSString *errorMsg;
// 用于檢查JSON是否合法
- (id)jsonValidator;
@end
該接口的具體實(shí)現(xiàn)為:
- (RACSignal *)enqueueRequest:(DDRequestInfo *)request method:(DDHTTPMethod)method resultClass:(Class)resultClass
{
@weakify(self);
// 封裝請(qǐng)求
return [[[self enqueueRequest:request method:method] reduceEach:^id _Nullable(NSHTTPURLResponse *response, id responseObject){
@strongify(self);
// 解析請(qǐng)求結(jié)果
return [[[self parsedResponseOfClass:resultClass fromJSON:responseObject] map:^id(id parsedResult) {
// 將結(jié)果封裝成DDResponse返回
DDResponse *parsedResponse = [[DDResponse alloc] initWithHTTPURLResponse:response parsedResult:parsedResult];
NSAssert(parsedResponse != nil, @"Could not create DDResponse with response %@ and parsedResult %@", response, parsedResult);
return parsedResponse;
}] doNext:^(DDResponse *parsedResponse) {
DDLOG(@"%@ => %li ", response.URL, (long)response.statusCode,);
}];
}] concat];
}
其中DDResponse為:
@interface DDResponse : NSObject
@property (nonatomic, strong, readonly) id parsedResult;
@property (nonatomic, assign, readonly) NSInteger statusCode;
- (instancetype)initWithHTTPURLResponse:(NSHTTPURLResponse *)response parsedResult:(id)parsedResult;
@end
封裝請(qǐng)求的實(shí)現(xiàn)如下:
- (RACSignal *)enqueueRequest:(DDRequestInfo *)request method:(DDHTTPMethod)method
{
@weakify(self);
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
@strongify(self);
NSURLSessionDataTask *task = nil;
if (DDHTTPMethodGET == method)
{
task = [self sendGETRequest:request success:^(NSHTTPURLResponse *response, id responseObject) {
if (response.statusCode == DDAPINotModifiedStatusCode)
{
[subscriber sendCompleted];
return ;
}
[[RACSignal return:RACTuplePack(response, responseObject)] subscribe:subscriber];
} andFalilur:^(NSError *error, NSInteger errCode) {
// 將網(wǎng)絡(luò)層的錯(cuò)誤向外傳遞
[subscriber sendError:error];
}];
}
else if (DDHTTPMethodPOST == method)
{
task = [self sendPOSTRequest:request success:^(NSHTTPURLResponse *response, id responseObject) {
if (response.statusCode == DDAPINotModifiedStatusCode)
{
[subscriber sendCompleted];
return ;
}
RACSignal *nextPageSignal = [RACSignal empty];
[[RACSignal return:RACTuplePack(response, responseObject)] subscribe:subscriber];
} andFalilur:^(NSError *error, NSInteger errCode) {
[subscriber sendError:error];
}];
}
return [RACDisposable disposableWithBlock:^{
// 需要cancel網(wǎng)絡(luò)請(qǐng)求。
if (task.state != NSURLSessionTaskStateCompleted) {
[task cancel];
}
}];
}];
return [[signal replayLazily] setNameWithFormat:@"-enqueueRequest:%@", request];
}
這里,第一、實(shí)現(xiàn)了將網(wǎng)絡(luò)層的錯(cuò)誤向外傳遞到視圖層。第二、實(shí)現(xiàn)了支持cancel網(wǎng)絡(luò)請(qǐng)求的功能。
解析請(qǐng)求結(jié)果的實(shí)現(xiàn):
- (RACSignal *)parsedResponseOfClass:(Class)resultClass fromJSON:(id)responseObject
{
@weakify(self);
return [RACSignal createSignal:^ id (id<RACSubscriber> subscriber) {
@strongify(self);
void (^parseJSONDictionary)(NSDictionary *) = ^(NSDictionary *JSONDictionary) {
@strongify(self);
if (resultClass == nil)
{
DDAPIResult *parsedObject = [[DDAPIResult class] mj_objectWithKeyValues:JSONDictionary];
// 檢查返回值的合法性
BOOL success = [self checkResult:JSONDictionary response:parsedObject];
if (success)
{
NSAssert([parsedObject isKindOfClass:DDObject.class], @"Parsed model object is not an DDObject: %@", parsedObject);
// 檢查API返回的錯(cuò)誤信息
if (parsedObject.errorCode > 0 || ![parsedObject.status isEqualToString:DDAPIResultStatusOK])
{
[subscriber sendError:[self errorFromServerResponse:JSONDictionary]];
return;
}
[subscriber sendNext:JSONDictionary];
}
else
{
NSString *failureReason = parsedObject.errorMsg.length > 0 ?parsedObject.errorMsg:[NSString stringWithFormat:NSLocalizedString(@"服務(wù)器返回的數(shù)據(jù)格式錯(cuò)誤! (%@): %@", @""), [responseObject class], responseObject];
[subscriber sendError:[self errorWithParsingFailureReason:failureReason]];
}
return;
}
DDAPIResult *parsedObject = [resultClass mj_objectWithKeyValues:JSONDictionary];
// 檢查返回值的合法性
BOOL success = [self checkResult:JSONDictionary response:parsedObject];
if (success)
{
NSAssert([parsedObject isKindOfClass:DDObject.class], @"Parsed model object is not an DDObject: %@", parsedObject);
// 檢查API返回的錯(cuò)誤信息
if (parsedObject.errorCode > 0 || ![parsedObject.status isEqualToString:DDAPIResultStatusOK])
{
[subscriber sendError:[self errorFromServerResponse:JSONDictionary]];
return;
}
[subscriber sendNext:parsedObject];
}
else
{
NSString *failureReason = parsedObject.errorMsg.length > 0 ?parsedObject.errorMsg:[NSString stringWithFormat:NSLocalizedString(@"服務(wù)器返回的數(shù)據(jù)格式錯(cuò)誤! (%@): %@", @""), [responseObject class], responseObject];
[subscriber sendError:[self errorWithParsingFailureReason:failureReason]];
return;
}
};
if ([responseObject isKindOfClass:NSArray.class])
{
for (NSDictionary *JSONDictionary in responseObject)
{
if (![JSONDictionary isKindOfClass:NSDictionary.class])
{
NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"Invalid JSON array element: %@", @""), JSONDictionary];
[subscriber sendError:[self errorWithParsingFailureReason:failureReason]];
return nil;
}
parseJSONDictionary(JSONDictionary);
}
[subscriber sendCompleted];
}
else if ([responseObject isKindOfClass:NSDictionary.class])
{
parseJSONDictionary(responseObject);
[subscriber sendCompleted];
}
else if (responseObject == nil)
{
[subscriber sendNext:nil];
[subscriber sendCompleted];
}
else
{
NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"Response wasn't an array or dictionary (%@): %@", @""), [responseObject class], responseObject];
[subscriber sendError:[self errorWithParsingFailureReason:failureReason]];
}
return nil;
}];
}
其中,DDObject為DDAPIResult的父類。json自動(dòng)轉(zhuǎn)model需要作傳入數(shù)據(jù)模型類型合法性檢查。
以上、網(wǎng)絡(luò)請(qǐng)求具備了RAC的能力。下一步就是在發(fā)起網(wǎng)絡(luò)請(qǐng)求的ViewModel中訂閱這些信號(hào)即可。