iOS 基于MVVM + RAC + ViewModel-Based Navigation的微信開發(fā)(二)

前言

這篇文章是iOS 基于MVVM + RAC + ViewModel-Based Navigation的微信開發(fā)(一)的續(xù)篇,主要目的就是繼續(xù)分析以下幾個(gè)核心問題,希望大家能知道其來龍去脈,并有所收獲,文章略長(zhǎng),先馬后看。

  • 項(xiàng)目中的網(wǎng)絡(luò)(Network)層解析。(√)
  • 搭建Debug調(diào)試工具。(待續(xù)...)
  • 項(xiàng)目中如何快速搭建類似發(fā)現(xiàn)我的設(shè)置、...等界面解析。
  • 如何利用該設(shè)計(jì)模式搭建游客模式(PS: 微信是登錄模式的架構(gòu))的架構(gòu)(待續(xù)...)。
  • 項(xiàng)目中的整體服務(wù)(Service)層解析。(待續(xù)...)
網(wǎng)絡(luò)(Network)層

網(wǎng)絡(luò)層在項(xiàng)目中扮演的角色,想必大家是心知肚明的,網(wǎng)絡(luò)層通過請(qǐng)求服務(wù)器的數(shù)據(jù),使得我們的應(yīng)用變得動(dòng)態(tài)性和有趣性。在微信(WeChat)Demo中,筆者主要賦予網(wǎng)絡(luò)層(MHHTTPService)的職責(zé)是:網(wǎng)絡(luò)數(shù)據(jù)(NetData)請(qǐng)求用戶數(shù)據(jù)(UserData)處理。當(dāng)然這只是筆者的一廂情愿罷了,大家肯定會(huì)有更好的職責(zé)和使命賦予網(wǎng)絡(luò)層的。

網(wǎng)絡(luò)數(shù)據(jù)(NetData)請(qǐng)求:目前絕大多數(shù)應(yīng)用都是使用AFNetworking來做網(wǎng)絡(luò)請(qǐng)求,當(dāng)然常規(guī)套路都是為了避免第三方框架的侵略性和耦合性,都會(huì)基于AFNetworking封裝成一個(gè)網(wǎng)絡(luò)工具類,暴露請(qǐng)求數(shù)據(jù)/上傳數(shù)據(jù)的API,以便后期使用的做法。例如:(XXHttpTool , XXNetworkTool , XXHttpHelper...)。如果項(xiàng)目比較復(fù)雜龐大的,數(shù)據(jù)請(qǐng)求可以集成YTKNetwork來實(shí)現(xiàn),其底層實(shí)現(xiàn)也是基于AFNetworking來封裝實(shí)現(xiàn)的,當(dāng)然蘿卜白菜,各有所愛,筆者主要是為了突出的是:封裝

筆者在WeChat項(xiàng)目中采用的是筆者熟悉的套路,基于AFNetworking 3.1.0封裝的一個(gè)網(wǎng)絡(luò)請(qǐng)求工具單例(MHHTTPService)。但可能與以往常規(guī)的網(wǎng)絡(luò)工具類的API,稍有差異,不要走開,請(qǐng)聽筆者慢慢道來。當(dāng)我們調(diào)用AFNetworkingGET/POST的方法請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)成功/失敗都是以block的形式傳遞出去的,所以平常網(wǎng)絡(luò)請(qǐng)求工具類封裝請(qǐng)求數(shù)據(jù)的API,也是通過block的形式傳遞數(shù)據(jù)的。類似于+ (void)get:(NSString *)url params:(NSDictionary *)params success:(void (^)(id responseObj))success failure:(void (^)(NSError *error))failure;這樣,但是將其使用在MVVM + RAC + ViewModel-Based Navigation里面,常規(guī)做法都是在MHTableViewModel的子類中重寫- (RACSignal *)requestRemoteDataSignalWithPage:(NSUInteger)page的方法來做數(shù)據(jù)請(qǐng)求,但是如果在該方法里面使用這種block的形式獲取數(shù)據(jù),就是有點(diǎn)顯得格格不入,讓人看著覺得別扭。

所以,最后筆者的做法是通過利用AFNetworking來做數(shù)據(jù)請(qǐng)求,而數(shù)據(jù)回調(diào)則使用ReactiveCocoa來傳遞數(shù)據(jù)信號(hào)(Signal),即:返回的數(shù)據(jù)是一個(gè)信號(hào)好RACSignal,這樣就優(yōu)雅的解決了上述Block返回?cái)?shù)據(jù)的尷尬。在設(shè)計(jì)微信(WeChat)網(wǎng)路工具類的API以及內(nèi)部實(shí)現(xiàn)時(shí),筆者主要參照OctoKitAPI來開發(fā)設(shè)計(jì)的,以及數(shù)據(jù)請(qǐng)求和數(shù)據(jù)回調(diào)信號(hào)(RACSignal)的內(nèi)部實(shí)現(xiàn)筆者主要參考的是AFNetworking-RACExtensionsOctoKit的實(shí)現(xiàn)方法,可謂是站在巨人的肩膀上開發(fā)的。大家有興趣可以看看其源碼,具體的細(xì)節(jié)還需自行體會(huì)。總之,最終的請(qǐng)求數(shù)據(jù)的方式筆者這里引用OctoKit的一段代碼如下:

// Prepares a request that will load all of the user's repositories, represented
// by `OCTRepository` objects.
//
// Note that the request is not actually _sent_ until you use one of the
// -subscribe… methods below.
RACSignal *request = [client fetchUserRepositories];
// This method actually kicks off the request, handling any results using the
// blocks below.
[request subscribeNext:^(OCTRepository *repository) {
    // This block is invoked for _each_ result received, so you can deal with
    // them one-by-one as they arrive.
} error:^(NSError *error) {
    // Invoked when an error occurs.
    //
    // Your `next` and `completed` blocks won't be invoked after this point.
} completed:^{
    // Invoked when the request completes and we've received/processed all the
    // results.
    //
    // Your `next` and `error` blocks won't be invoked after this point.
}];

當(dāng)然筆者的請(qǐng)求方式與OctoKit的又略有差異,這里筆者著重講述一下,筆者在設(shè)計(jì)這套網(wǎng)絡(luò)工具的思路和細(xì)節(jié)處理,當(dāng)然這肯定不是唯一的封裝方式,畢竟一千個(gè)人眼中有一千個(gè)潘金蓮(哈姆雷特)嘛,只是為大家提供一個(gè)參考而已。Let‘s Do It...

  • 代碼結(jié)構(gòu)
HTTPCodeStructure.png
  • 文件說明
    MHHTTPServiceConstant:常量定義。
    MHKeyedSubscript主要用來配置網(wǎng)絡(luò)請(qǐng)求參數(shù)字典,大家完全可以將其當(dāng)做字典(NSDictionary)來看待,當(dāng)然其本質(zhì)就是字典。這里只是筆者極不喜歡面向字典開發(fā),所以就將字典封裝在MHKeyedSubscript的內(nèi)部,提升了一丟丟的逼格罷了。具體應(yīng)用類似字典,這里不在贅述,代碼如下:

    MHKeyedSubscript *subscript = [MHKeyedSubscript subscript];
    subscript[@"useridx"] = useridx;
    subscript[@"type"] = @(type);
    subscript[@"page"] = @(page);
    

    MHURLParameters主要用來配置請(qǐng)求的基本參數(shù)、參數(shù)字典、請(qǐng)求路徑、請(qǐng)求方式等。具體內(nèi)容如下內(nèi)容如下:

    @interface MHURLParameters : NSObject
    /// 路徑 (v14/order)
    @property (nonatomic, readwrite, strong) NSString *path;
    /// 參數(shù)列表
    @property (nonatomic, readwrite, strong) NSDictionary *parameters;
    /// 方法 (POST/GET)
    @property (nonatomic, readwrite, strong) NSString *method;
    /// 拓展的參數(shù)屬性 (開發(fā)人員不必關(guān)心)
    @property (nonatomic, readwrite, strong) SBURLExtendsParameters *extendsParameters;
    
    /**
     參數(shù)配置(統(tǒng)一用這個(gè)方法配置參數(shù)) (SBBaseUrl : https://api.cleancool.tenqing.com/)
     https://api.cleancool.tenqing.com/user/info?user_id=100013
     @param method 方法名 (GET/POST/...)
     @param path 文件路徑 (user/info)
     @param parameters 具體參數(shù) @{user_id:10013}
     @return 返回一個(gè)參數(shù)實(shí)例
     */
    +(instancetype)urlParametersWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters;
    @end
    

    當(dāng)然筆者著重講一講基本參數(shù)(SBURLExtendsParameters)的配置,首先這個(gè)基本參數(shù)并不是每個(gè)服務(wù)器都要求配置的,完全根據(jù)你們后臺(tái)服務(wù)器來配置的。給大家看看我們公司的API文檔,截圖如下:

    BaseParameter.png

    所以,基本參數(shù)(SBURLExtendsParameters)的屬性字段設(shè)計(jì)成跟筆者公司的服務(wù)器字段一致即可,具體每個(gè)參數(shù)的作用和如何傳值,跟你們后臺(tái)人員協(xié)商即可。
    當(dāng)然項(xiàng)目中對(duì)于處理基本參數(shù)協(xié)議參數(shù)的做法無非就是:首先將基本參數(shù)協(xié)議參數(shù)通過拼接(addEntriesFromDictionary)成一個(gè)大字典(parameters),然后把parameters按照平常請(qǐng)求參數(shù)的拼接樣式key1=value1&key2=value2&key3=value3...拼接成一個(gè)參數(shù)字符串paramString,接著最重要的是將paramString拼接服務(wù)器的privatekeyprivateValue(PS:具體的私鑰)成帶私鑰的字符串(signedString),例如NSString *signedString = [NSString stringWithFormat:@"%@&privateKey=%@",paramString,MHHTTPServiceKeyValue];。其次通過對(duì)signedString進(jìn)行MD5加密得到簽名(sign)的值。最后將簽名(sign)的值添加到大字典(parameters)中parameters[@"sign"] = [sign length]?sign:@"";。最后得到的參數(shù)字典(parameters)里面包括基本參數(shù)協(xié)議參數(shù)的鍵值,以及最后增加的signsignValue。參考代碼如下:

    #pragma mark - Parameter 簽名 MD5 生成一個(gè) sign ,這里請(qǐng)根據(jù)實(shí)際項(xiàng)目來定
    /// 基礎(chǔ)的請(qǐng)求參數(shù)
    -(NSMutableDictionary *)_parametersWithRequest:(MHHTTPRequest *)request{
        NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
        /// 模型轉(zhuǎn)字典
        NSDictionary *extendsUrlParams = [request.urlParameters.extendsParameters mj_keyValues].copy;
        if ([extendsUrlParams count]) {
            [parameters addEntriesFromDictionary:extendsUrlParams];
        }
        if ([request.urlParameters.parameters count]) {
            [parameters addEntriesFromDictionary:request.urlParameters.parameters];
        }
        return parameters;
    }
    
    /// 帶簽名的請(qǐng)求參數(shù)
    -(NSString *)_signWithParameters:(NSDictionary *) parameters {
      /// 按照ASCII碼排序
        NSArray *sortedKeys = [[parameters allKeys] sortedArrayUsingSelector:@selector(compare:)];
      
        NSMutableArray *kvs = [NSMutableArray array];
        for (id key in sortedKeys) {
            /// value 為 empty 跳過
            if(MHObjectIsNil(parameters[key])) continue;
            NSString * value = [parameters[key] sb_stringValueExtension];
            if (MHObjectIsNil(value)||!MHStringIsNotEmpty(value)) continue;
            value = [value sb_removeBothEndsWhitespaceAndNewline];
            value = [value sb_URLEncoding];
            [kvs addObject:[NSString stringWithFormat:@"%@=%@",key,value]];
        }
        /// 拼接私鑰
        NSString *paramString = [kvs componentsJoinedByString:@"&"];
        NSString *keyValue = MHHTTPServiceKeyValue;
        NSString *signedString = [NSString stringWithFormat:@"%@&%@=%@",paramString,MHHTTPServiceKey,keyValue];
      
        /// md5
        return [CocoaSecurity md5:signedString].hexLower;
    }
    
    /// 序列化
    - (AFHTTPRequestSerializer *)_requestSerializerWithRequest:(MHHTTPRequest *) request{
        /// 獲取基礎(chǔ)參數(shù)(參數(shù)+拓展參數(shù))
        NSMutableDictionary *parameters = [self _parametersWithRequest:request];
        /// 獲取帶簽名的參數(shù)
        NSString *sign = [self _signWithParameters:parameters];
        /// 賦值
        parameters[MHHTTPServiceSignKey] = [sign length]?sign:@"";
        /// 請(qǐng)求序列化
        AFHTTPRequestSerializer *requestSerializer = [AFHTTPRequestSerializer serializer];
        /// 配置請(qǐng)求頭
        for (NSString *key in parameters) {
            NSString *value = [[parameters[key] sb_stringValueExtension] copy];
            if (value.length==0) continue;
            /// value只能是字符串,否則崩潰
            [requestSerializer setValue:value forHTTPHeaderField:key];
        }
        return requestSerializer;
    }
    

    當(dāng)然,最終的參數(shù)字典(parameters)使用一般有兩種做法(PS:請(qǐng)根據(jù)實(shí)際項(xiàng)目中服務(wù)端的要求來選用方法):
    方式一:將其添加到AFNetworking中的AFHTTPRequestSerializer的請(qǐng)求頭HTTPRequestHeaders中,通過- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(NSString *)field的方法實(shí)現(xiàn)對(duì)應(yīng)參數(shù)字典(parameters)的keyvalue的添加。具體代碼如下:

      /// 請(qǐng)求序列化
      AFHTTPRequestSerializer *requestSerializer = [AFHTTPRequestSerializer serializer];
      /// 配置請(qǐng)求頭
      for (NSString *key in parameters) {
          NSString *value = [[parameters[key] sb_stringValueExtension] copy];
          if (value.length==0) continue;
          /// value只能是字符串,否則崩潰
          [requestSerializer setValue:value forHTTPHeaderField:key];
      }
    

    最后,注意當(dāng)我們?cè)谑褂?code>AFNetworking的GET等方法(API)時(shí),需要將GET方法(API)的參數(shù)parameters,則傳遞的是協(xié)議參數(shù),而不是我們最終得到的參數(shù)字典(parameters)。
    方式二:直接將我們最終得到的參數(shù)字典(parameters)傳遞給AFNetworkingGET等方法(API)的參數(shù)parameters即可。

    MHHTTPRequest主要是通過MHURLParameters模型來配置請(qǐng)求模型。以及通過為MHHTTPRequest創(chuàng)建了分類,能夠在配置完請(qǐng)求模型完成,就可以直接發(fā)起MHHTTPService中的請(qǐng)求。起內(nèi)容如下:

    @interface MHHTTPRequest : NSObject
    /// 請(qǐng)求參數(shù)
    @property (nonatomic, readonly, strong) MHURLParameters *urlParameters;
    /**
     獲取請(qǐng)求類
     @param params  參數(shù)模型
     @return 請(qǐng)求類
     */
    +(instancetype)requestWithParameters:(MHURLParameters *)parameters;
    
    @end
    /// MHHTTPService的分類
    @interface MHHTTPRequest (MHHTTPService)
    /// 入隊(duì)
    - (RACSignal *) enqueueResultClass:(Class /*subclass of MHObject*/) resultClass;
    @end
    

    MHHTTPService整個(gè)網(wǎng)絡(luò)服務(wù)層(單例),繼承于AFHTTPSessionManager,主要用來做網(wǎng)絡(luò)數(shù)據(jù)請(qǐng)求用戶數(shù)據(jù)處理,這里筆者主要側(cè)重將的是其網(wǎng)絡(luò)數(shù)據(jù)請(qǐng)求。最關(guān)鍵的API如下:

    /**
     Enqueues a request to be sent to the server.
     This will automatically fetch a of the given endpoint. Each object
     from each page will be sent independently on the returned signal, so
     subscribers don't have to know or care about this pagination behavior.
    
     @param request config the request
     @param resultClass A subclass of `MHObject` that the response data should be returned as,
     and will be accessible from the `parsedResult`
     @return Returns a signal which will send an instance of `MHHTTPResponse` for each parsed
     JSON object, then complete. If an error occurs at any point,
     the returned signal will send it immediately, then terminate.
     */
    -(RACSignal *)enqueueRequest:(MHHTTPRequest *) request
                   resultClass:(Class /*subclass of MHObject*/) resultClass;
    

    通過執(zhí)行該方法,我們底層通過AFNetworking請(qǐng)求到JSON數(shù)據(jù),并通過YYModelJSON數(shù)據(jù)的data字段對(duì)應(yīng)的數(shù)據(jù)轉(zhuǎn)化為相應(yīng)的resultClass,并最終包裹成MHHTTPResponse數(shù)據(jù),然后通過ReactiveCocoa轉(zhuǎn)化成數(shù)據(jù)信號(hào)并返回的過程。當(dāng)然平常大家在通過網(wǎng)絡(luò)請(qǐng)求工具做數(shù)據(jù)請(qǐng)求時(shí),回調(diào)的數(shù)據(jù)絕大多數(shù)都是JSON數(shù)據(jù)(id responseObject),然后在對(duì)應(yīng)的控制器里面做字典轉(zhuǎn)模型操作。當(dāng)然筆者這種做法很依賴服務(wù)器返回的數(shù)據(jù)格式,顯然前提是你需要和你的服務(wù)端人員共同協(xié)商好一份合適數(shù)據(jù)返回格式,然后再來設(shè)計(jì)這套網(wǎng)絡(luò)工具。首先常用JSON數(shù)據(jù)最外層是一個(gè)字典,且字段主要是:codemsgdata

    code: 請(qǐng)求狀態(tài)碼。比如100:請(qǐng)求成功101:對(duì)應(yīng)參數(shù)有誤...
    msg: 請(qǐng)求狀態(tài)說明,主要是對(duì)code對(duì)應(yīng)的值的解釋。比如請(qǐng)求成功點(diǎn)贊成功...
    data請(qǐng)求的數(shù)據(jù),且其對(duì)應(yīng)的數(shù)據(jù)也是一個(gè)字典({})。YYModel主要對(duì)該字段對(duì)應(yīng)的數(shù)據(jù)做字典轉(zhuǎn)模型處理,比如用戶數(shù)據(jù),商品列表...

    這里筆者用偽代碼的形式詳述筆者與后臺(tái)協(xié)商的三種JSON數(shù)據(jù)格式(PS:主要是data對(duì)應(yīng)的數(shù)據(jù)變化,以及我們著重需要其內(nèi)部那些重要數(shù)據(jù))。

    格式一: data對(duì)應(yīng)的只是單個(gè)字典數(shù)據(jù),比如用戶模型...

    {
      "code" : "100",
      "msg": "請(qǐng)求成功",
      "data":{
          "user_id" : "100013",
          "avatar" : "https://...",
          "nickname": "CodeMikeHe",
          ...
      }
    }
    

    類似這種情況請(qǐng)求數(shù)據(jù)時(shí),則resultClass,傳[MHUser class]即可。則底層就會(huì)通過YYModeldata對(duì)應(yīng)的字典轉(zhuǎn)化成用戶模型(MHUser`)。

    格式二: data對(duì)應(yīng)的是字典且我們只需要該字典的list對(duì)應(yīng)的數(shù)組([])列表,比如直播間列表...

    {
      "code" : "100",
      "msg": "請(qǐng)求成功",
      "data":{
          "list" : [
              {/** 直播間數(shù)據(jù) */},
              {/** 直播間數(shù)據(jù)*/},
              {/** 直播間數(shù)據(jù) */},
              {/** 直播間數(shù)據(jù) */},
              ...
          ],
          "totalPage":4,
         "samecity":0,
         "hotswitch":null,
         "hotswitch2":Array[0],
         "hotConfig":0
          ...
      }
    }
    

    如果我們只想要獲取data[@"list"]對(duì)應(yīng)的數(shù)據(jù)列表,比如筆者Demo中的首頁(yè)數(shù)據(jù)展示。那么首先我們必須要和后臺(tái)人員協(xié)商好,后期遇到這種列表(數(shù)組)的情況,必須是list這個(gè)字段對(duì)應(yīng)數(shù)組列表即可。所以類似這種情況,則resultClass,傳[MHLive class]即可。則底層就會(huì)通過YYModeldata對(duì)應(yīng)list的列表轉(zhuǎn)化成直播間模型數(shù)組的。當(dāng)然如果你還想要data字典中的其他值,那么你就把這個(gè)當(dāng)做 格式一 的方式去處理即可。靈活使用,才是關(guān)鍵。

    格式三: data對(duì)應(yīng)的是空值,即<nil>,比如用戶點(diǎn)贊,一般不需要返回?cái)?shù)據(jù),因?yàn)槲覀兺ㄟ^code = 100就可以判斷是否點(diǎn)贊成功,一般這種data是個(gè)空值。...

    {
      "code" : "100",
      "msg": "點(diǎn)贊成功",
      "data": <nil>,
      
    }
    

    類似這種情況,則resultClassnil即可,這樣筆者會(huì)原封不動(dòng)的把后臺(tái)的數(shù)據(jù)返回出去。而你只需要根據(jù)code的值來做相應(yīng)的提示即可。
    需要注意的是: resultClass必須是MHObject的子類,或者為nil。否則會(huì)Crash掉。

    MHHTTPResponse主要是請(qǐng)求成功后返回的服務(wù)器數(shù)據(jù)模型,主要是將服務(wù)器最外層的數(shù)據(jù)(字典),剝離出來而已。其頭文件內(nèi)容如下:

    @interface MHHTTPResponse : MHObject
    /// The parsed MHObject object corresponding to the API response.
    /// The developer need care this data 切記:若沒有數(shù)據(jù)是NSNull 而不是nil .對(duì)應(yīng)于服務(wù)器json數(shù)據(jù)的 data
    @property (nonatomic, readonly, strong) id parsedResult;
    /// 自己服務(wù)器返回的狀態(tài)碼 對(duì)應(yīng)于服務(wù)器json數(shù)據(jù)的 code
    @property (nonatomic, readonly, assign) MHHTTPResponseCode code;
    /// 自己服務(wù)器返回的信息 對(duì)應(yīng)于服務(wù)器json數(shù)據(jù)的 code
    @property (nonatomic, readonly, copy) NSString *msg;
    
    // Initializes the receiver with the headers from the given response, and given the origin data and the
    // given parsed model object(s).
    - (instancetype)initWithResponseObject:(id)responseObject parsedResult:(id)parsedResult;
    @end
    

    這里的屬性與服務(wù)器返回的字段保持一致,只不過用parsedResult代替data罷了。這里需要強(qiáng)調(diào)的是,當(dāng)我們?cè)谡{(diào)用-(RACSignal *)enqueueRequest:(MHHTTPRequest *) request resultClass:(Class /*subclass of MHObject*/) resultClass;時(shí),resultClass參數(shù)如果我們傳nil,那么筆者底層將不會(huì)利用YYModel去把data數(shù)據(jù)轉(zhuǎn)化成模型,而是原封不動(dòng)的服務(wù)器的data數(shù)據(jù)賦值到parsedResult。當(dāng)然,格式一對(duì)應(yīng)的parsedResultMHUser模型;格式二對(duì)應(yīng)的parsedResultNSArray <MHLiveRoom *> * parsedResult模型數(shù)組;特別強(qiáng)調(diào)的是格式三那種情況,則parsedResultNSNull對(duì)象,而不是nil。這里需要注意的!!!。

    RACSignal+MHHTTPServiceAdditions主要作用是解析MHHTTPResponse數(shù)據(jù),通過ReactiveCocoamap方法,將MHHTTPResponseparsedResult映射出來。關(guān)鍵代碼如下:

    - (RACSignal *)mh_parsedResults {
       return [self map:^(MHHTTPResponse *response) {
         NSAssert([response isKindOfClass:MHHTTPResponse.class], @"Expected %@ to be an MHHTTPResponse.", response);
           return response.parsedResult;
       }];
    }
    

    首先開發(fā)中我們通過-(RACSignal *)enqueueRequest:(MHHTTPRequest *) request resultClass:(Class /*subclass of MHObject*/) resultClass;這個(gè)方法返回的一個(gè)數(shù)據(jù)信號(hào)resultSignal,如果訂閱(subscribeNext)該數(shù)據(jù)信號(hào)resultSignal其值是MHHTTPResponse。即偽代碼如下:

    /// 請(qǐng)求的數(shù)據(jù)信號(hào) (PS:當(dāng)然可以一句代碼搞定,這里只做演示)
    RACSignal *resultSignal = [[MHHTTPService sharedInstance] enqueueRequest:request resultClass:[MHUser class]];
    /// 訂閱數(shù)據(jù)信號(hào)
    [resultSignal subscribeNext:^(MHHTTPResponse * response) {
     /// 成功回調(diào) response.parsedResult 為MHUser模型。
    
     } error:^(NSError *error) {
     /// 失敗回調(diào)
    
     } completed:^{
     /// 完成
    
     }];
    

    在開發(fā)中,我們主要是想要獲取的是data對(duì)應(yīng)的數(shù)據(jù)(PS:即response.parsedResult的值)。而很少去關(guān)注最外層的codemsg對(duì)應(yīng)的值。所以,就出現(xiàn)了mh_parsedResults來直接獲取response.parsedResult的值。所以上面的偽代碼可以改成以下:

    [[[[MHHTTPService sharedInstance] enqueueRequest:request resultClass:[MHUser class]] 
    mh_parsedResults] 
    subscribeNext:^(MHUser * user) {
     /// 成功回調(diào) MHUser模型。
    
     } error:^(NSError *error) {
     /// 失敗回調(diào)
    
     } completed:^{
     /// 完成
    
     }];
    

    這樣是不是覺得高端大氣上檔次,低調(diào)奢華有內(nèi)涵。當(dāng)然特別需要注意的是,不是每一個(gè)信號(hào)(Signal),都可以調(diào)用mh_parsedResults,必須是訂閱(subscribeNext)該數(shù)據(jù)信號(hào)resultSignal其值是MHHTTPResponse才行,否則程序Crash

  • 關(guān)于使用
    這里筆者將喵播的熱門數(shù)據(jù)的API https://live.9158.com/Room/GetHotLive_v2?cache=3&lat=22.54192103514200&lon=113.96939828211362&page=1&province=%E5%B9%BF%E4%B8%9C%E7%9C%81&type=0&useridx=61856069為例,講講開發(fā)中如何具體使用一下MHHTTPService。代碼如下:

    /// 獲取直播間列表
    - (RACSignal *)fetchLivesWithUseridx:(NSString *)useridx type:(NSInteger)type page:(NSInteger)page lat:(NSNumber *)lat lon:(NSNumber *)lon province:(NSString *)province{
        /// 1. 配置參數(shù)
        MHKeyedSubscript *subscript = [MHKeyedSubscript subscript];
        subscript[@"useridx"] = useridx;
        subscript[@"type"] = @(type);
        subscript[@"page"] = @(page);
        if (lat == nil) subscript[@"lat"] = @(22.54192103514200);
        if (lon == nil) subscript[@"lon"] = @(113.96939828211362);
        if (province == nil) subscript[@"province"] = @"廣東省";
      
        /// 2. 配置參數(shù)模型 #define MH_GET_LIVE_ROOM_LIST  @"Room/GetHotLive_v2"
        MHURLParameters *paramters = [MHURLParameters urlParametersWithMethod:MH_HTTTP_METHOD_GET path:MH_GET_LIVE_ROOM_LIST parameters:subscript.dictionary];
      
        /// 3.發(fā)起請(qǐng)求 如果你想獲取data的數(shù)據(jù)而不是data[@"list"]的數(shù)據(jù),則resultClass為`[MHLiveInfo class]`即可。
        return [[[MHHTTPRequest requestWithParameters:paramters]
               enqueueResultClass:[MHLiveRoom class]]
              mh_parsedResults];
    }
    

    當(dāng)然,上面的步驟三(發(fā)起請(qǐng)求),其實(shí)正常情況下應(yīng)該為下面的兩步:

      /// 配置請(qǐng)求模型
      MHHTTPRequest *request = [MHHTTPRequest requestWithParameters:paramters];
      /// 發(fā)起請(qǐng)求
      return [[MHHTTPService sharedInstance] enqueueRequest:request resultClass:[MHLiveRoom class]];
    

    但是由于其過于繁瑣,筆者通過為MHHTTPRequest創(chuàng)建了分類,能夠在配置完請(qǐng)求模型完成,就可以直接發(fā)起MHHTTPService中的請(qǐng)求,這樣就優(yōu)雅的實(shí)現(xiàn)了化二為一的效果。關(guān)鍵代碼如下:

    /// 網(wǎng)絡(luò)服務(wù)層分類 方便MHHTTPRequest 主動(dòng)發(fā)起請(qǐng)求
    @implementation MHHTTPRequest (MHHTTPService)
    /// 請(qǐng)求數(shù)據(jù)
    -(RACSignal *) enqueueResultClass:(Class /*subclass of MHObject*/) resultClass {
        return [[MHHTTPService sharedInstance] enqueueRequest:self resultClass:resultClass];
    }
    @end
    

    當(dāng)然,細(xì)節(jié)注意的地方就是網(wǎng)路請(qǐng)求工具盡量將其作為MHHTTPService的分類來設(shè)計(jì),且命名要規(guī)范,并與請(qǐng)求成功后的模型放在同一個(gè)文件夾,這樣更好的提現(xiàn)單一職責(zé)化。比如:請(qǐng)求的用戶數(shù)據(jù),分類名稱為:MHHTTPService+User,主要負(fù)責(zé)的是: 請(qǐng)求用戶數(shù)據(jù), 修改用戶信息 , .... 等API。

    調(diào)試細(xì)節(jié)注意:在開發(fā)過程中,我們可能事先對(duì)服務(wù)器的返回的數(shù)據(jù)還一無所知,這樣就無法新建模型,這時(shí)候建議先將resultClassnil,然后打印數(shù)據(jù)即可。更多細(xì)節(jié)還請(qǐng)查看筆者提供的Demo。

  • 錯(cuò)誤處理
    請(qǐng)求服務(wù)器數(shù)據(jù)出錯(cuò)在開發(fā)中可謂是家常便飯了,為了提高用戶體驗(yàn),我們前端必須處理和解析錯(cuò)誤信息(NSError),以便我們更好的根據(jù)錯(cuò)誤信息展示不同的UI以及顯示錯(cuò)誤提示。當(dāng)然,相信大部分開發(fā)者,都沒怎么好好處理AFNetworking請(qǐng)求的錯(cuò)誤信息,都是在AFNetworking的請(qǐng)求錯(cuò)誤的block里面,提示一個(gè)服務(wù)器不給力,請(qǐng)稍后重試網(wǎng)絡(luò)有問題,請(qǐng)稍后重試...等等。當(dāng)然有提示總比沒提示強(qiáng),有些開發(fā)者根本不處理錯(cuò)誤信息,頂多是在AFNetworking的請(qǐng)求錯(cuò)誤的block里面NSLog一下錯(cuò)誤,這樣用戶體驗(yàn)極其不佳。當(dāng)然,前面的提示也是不準(zhǔn)確的,或者說不友好。比如有時(shí)候是服務(wù)器有問題,你卻提示網(wǎng)絡(luò)有問題,請(qǐng)稍后重試;又或者是用戶網(wǎng)絡(luò)斷開連接,而你卻提示服務(wù)器不給力,請(qǐng)稍后重試等。當(dāng)然,我們也沒必要把錯(cuò)誤信息(NSError)整個(gè)提示出來,這樣也會(huì)導(dǎo)致你提示出來的信息可能是一大堆亂七八糟的英文信息,比如錯(cuò)誤信息如下:

    {fails Error Domain=NSCocoaErrorDomain Code=3840 "JSON text did not start with array or object and option to allow fragments not set.
    " UserInfo={NSDebugDescription=JSON text did not start with array or object and option to allow fragments not set.}
    

    可能有些錯(cuò)誤,我們開發(fā)者自己都不清楚其原因,你覺得用戶會(huì)知道錯(cuò)誤原因嗎?所以,最主要的是提示準(zhǔn)確且簡(jiǎn)單的錯(cuò)誤信息,筆者的做法如下對(duì)于AFNetworking的錯(cuò)誤(NSError),筆者這里只分為三種情況:①服務(wù)器請(qǐng)求出錯(cuò)②請(qǐng)求超時(shí)③網(wǎng)路斷開連接。當(dāng)然在開發(fā)人員調(diào)試(DEBUG)狀態(tài)下,筆者是會(huì)將錯(cuò)誤碼一同提示出來,方便開發(fā)人員準(zhǔn)確定位錯(cuò)誤信息。當(dāng)然在發(fā)布(Release)狀態(tài)下,是不會(huì)提示錯(cuò)誤碼的。關(guān)鍵代碼如下所述:

    #pragma mark - Error Handling
    /// 請(qǐng)求錯(cuò)誤解析
    - (NSError *)_errorFromRequestWithTask:(NSURLSessionTask *)task httpResponse:(NSHTTPURLResponse *)httpResponse responseObject:(NSDictionary *)responseObject error:(NSError *)error {
        /// 不一定有值,則HttpCode = 0;
        NSInteger HTTPCode = httpResponse.statusCode;
        NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
        /// default errorCode is MHHTTPServiceErrorConnectionFailed,意味著連接不上服務(wù)器
      NSInteger errorCode = MHHTTPServiceErrorConnectionFailed;
      NSString *errorDesc = @"服務(wù)器出錯(cuò)了,請(qǐng)稍后重試~";
      /// 其實(shí)這里需要處理后臺(tái)數(shù)據(jù)錯(cuò)誤,一般包在 responseObject
      /// HttpCode錯(cuò)誤碼解析 https://www.guhei.net/post/jb1153
      /// 1xx : 請(qǐng)求消息 [100  102]
      /// 2xx : 請(qǐng)求成功 [200  206]
      /// 3xx : 請(qǐng)求重定向[300  307]
      /// 4xx : 請(qǐng)求錯(cuò)誤  [400  417] 、[422 426] 、449、451
      /// 5xx 、600: 服務(wù)器錯(cuò)誤 [500 510] 、600
      NSInteger httpFirstCode = HTTPCode/100;
      if (httpFirstCode>0) {
          if (httpFirstCode==4) {
              /// 請(qǐng)求出錯(cuò)了,請(qǐng)稍后重試
              if (HTTPCode == 408) {
    #if defined(DEBUG)||defined(_DEBUG)
                  errorDesc = @"請(qǐng)求超時(shí),請(qǐng)稍后再試(408)~"; /// 調(diào)試模式
    #else
                  errorDesc = @"請(qǐng)求超時(shí),請(qǐng)稍后再試~";      /// 發(fā)布模式
    #endif
              }else{
    #if defined(DEBUG)||defined(_DEBUG)
                  errorDesc = [NSString stringWithFormat:@"請(qǐng)求出錯(cuò)了,請(qǐng)稍后重試(%zd)~",HTTPCode];                   /// 調(diào)試模式
    #else
                  errorDesc = @"請(qǐng)求出錯(cuò)了,請(qǐng)稍后重試~";      /// 發(fā)布模式
    #endif
                }
            }else if (httpFirstCode == 5 || httpFirstCode == 6){
                /// 服務(wù)器出錯(cuò)了,請(qǐng)稍后重試
    #if defined(DEBUG)||defined(_DEBUG)
              errorDesc = [NSString stringWithFormat:@"服務(wù)器出錯(cuò)了,請(qǐng)稍后重試(%zd)~",HTTPCode];                      /// 調(diào)試模式
    #else
              errorDesc = @"服務(wù)器出錯(cuò)了,請(qǐng)稍后重試~";       /// 發(fā)布模式
    #endif
              
          }else if (!self.reachabilityManager.isReachable){
              /// 網(wǎng)絡(luò)不給力,請(qǐng)檢查網(wǎng)絡(luò)
              errorDesc = @"網(wǎng)絡(luò)開小差了,請(qǐng)稍后重試~";
          }
      }else{
          if (!self.reachabilityManager.isReachable){
              /// 網(wǎng)絡(luò)不給力,請(qǐng)檢查網(wǎng)絡(luò)
              errorDesc = @"網(wǎng)絡(luò)開小差了,請(qǐng)稍后重試~";
          }
      }
      switch (HTTPCode) {
          case 400:{
              errorCode = MHHTTPServiceErrorBadRequest;           /// 請(qǐng)求失敗
              break;
          }
          case 403:{
              errorCode = MHHTTPServiceErrorRequestForbidden;     /// 服務(wù)器拒絕請(qǐng)求
              break;
          }
          case 422:{
              errorCode = MHHTTPServiceErrorServiceRequestFailed; /// 請(qǐng)求出錯(cuò)
              break;
          }
          default:
              /// 從error中解析
              if ([error.domain isEqual:NSURLErrorDomain]) {
    #if defined(DEBUG)||defined(_DEBUG)
                  errorDesc = [NSString stringWithFormat:@"請(qǐng)求出錯(cuò)了,請(qǐng)稍后重試(%zd)~",error.code];                   /// 調(diào)試模式
    #else
                  errorDesc = @"請(qǐng)求出錯(cuò)了,請(qǐng)稍后重試~";        /// 發(fā)布模式
    #endif
                  switch (error.code) {
                      case NSURLErrorSecureConnectionFailed:
                      case NSURLErrorServerCertificateHasBadDate:
                      case NSURLErrorServerCertificateHasUnknownRoot:
                      case NSURLErrorServerCertificateUntrusted:
                      case NSURLErrorServerCertificateNotYetValid:
                      case NSURLErrorClientCertificateRejected:
                      case NSURLErrorClientCertificateRequired:
                          errorCode = MHHTTPServiceErrorSecureConnectionFailed; /// 建立安全連接出錯(cuò)了
                          break;
                      case NSURLErrorTimedOut:{
    #if defined(DEBUG)||defined(_DEBUG)
                          errorDesc = @"請(qǐng)求超時(shí),請(qǐng)稍后再試(-1001)~"; /// 調(diào)試模式
    #else
                          errorDesc = @"請(qǐng)求超時(shí),請(qǐng)稍后再試~";        /// 發(fā)布模式
    #endif
                          break;
                      }
                      case NSURLErrorNotConnectedToInternet:{
    #if defined(DEBUG)||defined(_DEBUG)
                          errorDesc = @"網(wǎng)絡(luò)開小差了,請(qǐng)稍后重試(-1009)~"; /// 調(diào)試模式
    #else
                          errorDesc = @"網(wǎng)絡(luò)開小差了,請(qǐng)稍后重試~";        /// 發(fā)布模式
    #endif
                          break;
                      }
                  }
              }
      }
      userInfo[MHHTTPServiceErrorHTTPStatusCodeKey] = @(HTTPCode);
      userInfo[MHHTTPServiceErrorDescriptionKey] = errorDesc;
      if (task.currentRequest.URL != nil) userInfo[MHHTTPServiceErrorRequestURLKey] = task.currentRequest.URL.absoluteString;
      if (task.error != nil) userInfo[NSUnderlyingErrorKey] = task.error;
      return [NSError errorWithDomain:MHHTTPServiceErrorDomain code:errorCode userInfo:userInfo];
    }
    

    當(dāng)然,還有一種錯(cuò)誤處理就是利用AFNetworking請(qǐng)求數(shù)據(jù)成功,但是后臺(tái)反饋/驗(yàn)證錯(cuò)誤信息(msg)。假設(shè)code = 100為獲取數(shù)據(jù)成功 , 而其他code ≠ 100的都是錯(cuò)誤,且對(duì)應(yīng)錯(cuò)誤信息msg字段。這個(gè)我們也需要處理,并且也得在調(diào)試模式下把code提示出來,以便后臺(tái)開發(fā)人員根據(jù)code的值來定位BUG。處理代碼如下:

    {
                          
                          NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
                          userInfo[MHHTTPServiceErrorResponseCodeKey] = @(statusCode);
                          NSString *msgTips = responseObject[MHHTTPServiceResponseMsgKey];
    #if defined(DEBUG)||defined(_DEBUG)
                          msgTips = MHStringIsNotEmpty(msgTips)?[NSString stringWithFormat:@"%@(%zd)",msgTips,statusCode]:[NSString stringWithFormat:@"服務(wù)器出錯(cuò)了,請(qǐng)稍后重試(%zd)~",statusCode];                 /// 調(diào)試模式
    #else
                          msgTips = MHStringIsNotEmpty(msgTips)?msgTips:@"服務(wù)器出錯(cuò)了,請(qǐng)稍后重試~";  /// 發(fā)布模式
    #endif
                          userInfo[MHHTTPServiceErrorMessagesKey] = msgTips;
                          if (task.currentRequest.URL != nil) userInfo[MHHTTPServiceErrorRequestURLKey] = task.currentRequest.URL.absoluteString;
                          if (task.error != nil) userInfo[NSUnderlyingErrorKey] = task.error;
                          [subscriber sendError:[NSError errorWithDomain:MHHTTPServiceErrorDomain code:statusCode userInfo:userInfo]];
    }
    

    這樣一來,到時(shí)候我們提示錯(cuò)誤信息就變得so easy。例如筆者的在項(xiàng)目中就利用MBProgressHUD來提示錯(cuò)誤,當(dāng)然筆者也為該錯(cuò)誤(NSError)的解析提供了分類:關(guān)鍵代碼如下:

    + (NSString *)mh_tipsFromError:(NSError *)error{
        if (!error) return nil;
        NSString *tipStr = nil;
        /// 這里需要處理HTTP請(qǐng)求的錯(cuò)誤
        if (error.userInfo[MHHTTPServiceErrorDescriptionKey]) {
            tipStr = [error.userInfo objectForKey:MHHTTPServiceErrorDescriptionKey];
        }else if (error.userInfo[MHHTTPServiceErrorMessagesKey]) {
            tipStr = [error.userInfo objectForKey:MHHTTPServiceErrorMessagesKey];
        }else if (error.domain) {
            tipStr = error.localizedFailureReason;
        } else {
            tipStr = error.localizedDescription;
        }
        return tipStr;
    }
    
期待
  1. 文章若對(duì)您有些許幫助,請(qǐng)給個(gè)喜歡??,畢竟碼字不易;若對(duì)您沒啥幫助,請(qǐng)給點(diǎn)建議??,切記學(xué)無止境。
  2. 針對(duì)文章所述內(nèi)容,閱讀期間任何疑問;請(qǐng)?jiān)谖恼碌撞吭u(píng)論指出,我會(huì)火速解決和修正問題。
  3. GitHub地址:https://github.com/CoderMikeHe
  4. 源碼地址:WeChat
參考鏈接
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容