App架構(gòu)升級(jí)之網(wǎng)絡(luò)層優(yōu)化(一)

一、背景

目前,公司里的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ò)NSHTTPURLResponsestatusCode來(lái)決定是否處理返回的數(shù)據(jù)。比如statusCode304的時(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)即可。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容