前言
這篇文章是iOS 基于MVVM + RAC + ViewModel-Based Navigation的微信開發(一)的續篇,主要目的就是繼續分析以下幾個核心問題,希望大家能知道其來龍去脈,并有所收獲,文章略長,先馬后看。
- 項目中的網絡(
Network
)層解析。(√) - 搭建
Debug
調試工具。(待續...) - 項目中如何快速搭建類似
發現
、我的
、設置
、...等界面解析。 - 如何利用該設計模式搭建
游客模式
(PS: 微信是登錄模式的架構)的架構(待續...)。 - 項目中的整體服務(
Service
)層解析。(待續...)
網絡(Network
)層
網絡層在項目中扮演的角色,想必大家是心知肚明的,網絡層通過請求服務器的數據,使得我們的應用變得動態性和有趣性。在微信(WeChat)
Demo中,筆者主要賦予網絡層(MHHTTPService
)的職責是:網絡數據(NetData)請求和用戶數據(UserData)處理。當然這只是筆者的一廂情愿罷了,大家肯定會有更好的職責和使命賦予網絡層的。
網絡數據(NetData)請求:目前絕大多數應用都是使用AFNetworking來做網絡請求,當然常規套路都是為了避免第三方框架的侵略性和耦合性,都會基于AFNetworking
封裝成一個網絡工具類,暴露請求數據/上傳數據的API,以便后期使用的做法。例如:(XXHttpTool
, XXNetworkTool
, XXHttpHelper
...)。如果項目比較復雜龐大的,數據請求可以集成YTKNetwork來實現,其底層實現也是基于AFNetworking
來封裝實現的,當然蘿卜白菜,各有所愛,筆者主要是為了突出的是:封裝。
筆者在WeChat
項目中采用的是筆者熟悉的套路,基于AFNetworking 3.1.0
封裝的一個網絡請求工具單例(MHHTTPService
)。但可能與以往常規的網絡工具類的API,稍有差異,不要走開,請聽筆者慢慢道來。當我們調用AFNetworking
的GET/POST
的方法請求網絡數據成功/失敗都是以block
的形式傳遞出去的,所以平常網絡請求工具類封裝請求數據的API,也是通過block
的形式傳遞數據的。類似于+ (void)get:(NSString *)url params:(NSDictionary *)params success:(void (^)(id responseObj))success failure:(void (^)(NSError *error))failure;
這樣,但是將其使用在MVVM + RAC + ViewModel-Based Navigation
里面,常規做法都是在MHTableViewModel
的子類中重寫- (RACSignal *)requestRemoteDataSignalWithPage:(NSUInteger)page
的方法來做數據請求,但是如果在該方法里面使用這種block
的形式獲取數據,就是有點顯得格格不入,讓人看著覺得別扭。
所以,最后筆者的做法是通過利用AFNetworking來做數據請求,而數據回調則使用ReactiveCocoa來傳遞數據信號(Signal
),即:返回的數據是一個信號好RACSignal
,這樣就優雅的解決了上述Block
返回數據的尷尬。在設計微信(WeChat
)網路工具類的API以及內部實現時,筆者主要參照OctoKit的API
來開發設計的,以及數據請求和數據回調信號(RACSignal
)的內部實現筆者主要參考的是AFNetworking-RACExtensions和OctoKit的實現方法,可謂是站在巨人的肩膀上開發的。大家有興趣可以看看其源碼,具體的細節還需自行體會。總之,最終的請求數據的方式筆者這里引用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.
}];
當然筆者的請求方式與OctoKit
的又略有差異,這里筆者著重講述一下,筆者在設計這套網絡工具的思路和細節處理,當然這肯定不是唯一的封裝方式,畢竟一千個人眼中有一千個潘金蓮(哈姆雷特)嘛,只是為大家提供一個參考而已。Let‘s Do It...
- 代碼結構
-
文件說明
MHHTTPServiceConstant:常量定義。
MHKeyedSubscript主要用來配置網絡請求參數字典,大家完全可以將其當做字典(NSDictionary
)來看待,當然其本質就是字典。這里只是筆者極不喜歡面向字典開發,所以就將字典封裝在MHKeyedSubscript
的內部,提升了一丟丟的逼格罷了。具體應用類似字典,這里不在贅述,代碼如下:MHKeyedSubscript *subscript = [MHKeyedSubscript subscript]; subscript[@"useridx"] = useridx; subscript[@"type"] = @(type); subscript[@"page"] = @(page);
MHURLParameters主要用來配置請求的基本參數、參數字典、請求路徑、請求方式等。具體內容如下內容如下:
@interface MHURLParameters : NSObject /// 路徑 (v14/order) @property (nonatomic, readwrite, strong) NSString *path; /// 參數列表 @property (nonatomic, readwrite, strong) NSDictionary *parameters; /// 方法 (POST/GET) @property (nonatomic, readwrite, strong) NSString *method; /// 拓展的參數屬性 (開發人員不必關心) @property (nonatomic, readwrite, strong) SBURLExtendsParameters *extendsParameters; /** 參數配置(統一用這個方法配置參數) (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 具體參數 @{user_id:10013} @return 返回一個參數實例 */ +(instancetype)urlParametersWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters; @end
當然筆者著重講一講
基本參數(SBURLExtendsParameters)
的配置,首先這個基本參數
并不是每個服務器都要求配置的,完全根據你們后臺服務器來配置的。給大家看看我們公司的API文檔,截圖如下:BaseParameter.png
所以,基本參數(SBURLExtendsParameters)
的屬性字段設計成跟筆者公司的服務器字段一致即可,具體每個參數的作用和如何傳值,跟你們后臺人員協商即可。
當然項目中對于處理基本參數
和協議參數
的做法無非就是:首先將基本參數
和協議參數
通過拼接(addEntriesFromDictionary
)成一個大字典(parameters
),然后把parameters
按照平常請求參數的拼接樣式key1=value1&key2=value2&key3=value3...
拼接成一個參數字符串paramString
,接著最重要的是將paramString
拼接服務器的privatekey
和privateValue
(PS:具體的私鑰)成帶私鑰的字符串(signedString
),例如NSString *signedString = [NSString stringWithFormat:@"%@&privateKey=%@",paramString,MHHTTPServiceKeyValue];
。其次通過對signedString
進行MD5
加密得到簽名(sign
)的值。最后將簽名(sign
)的值添加到大字典(parameters
)中parameters[@"sign"] = [sign length]?sign:@"";
。最后得到的參數字典(parameters
)里面包括基本參數
和協議參數
的鍵值,以及最后增加的sign
和signValue
。參考代碼如下:#pragma mark - Parameter 簽名 MD5 生成一個 sign ,這里請根據實際項目來定 /// 基礎的請求參數 -(NSMutableDictionary *)_parametersWithRequest:(MHHTTPRequest *)request{ NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; /// 模型轉字典 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; } /// 帶簽名的請求參數 -(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{ /// 獲取基礎參數(參數+拓展參數) NSMutableDictionary *parameters = [self _parametersWithRequest:request]; /// 獲取帶簽名的參數 NSString *sign = [self _signWithParameters:parameters]; /// 賦值 parameters[MHHTTPServiceSignKey] = [sign length]?sign:@""; /// 請求序列化 AFHTTPRequestSerializer *requestSerializer = [AFHTTPRequestSerializer serializer]; /// 配置請求頭 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; }
當然,最終的參數字典(
parameters
)使用一般有兩種做法(PS:請根據實際項目中服務端的要求來選用方法):
方式一:將其添加到AFNetworking
中的AFHTTPRequestSerializer
的請求頭HTTPRequestHeaders
中,通過- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(NSString *)field
的方法實現對應參數字典(parameters
)的key
和value
的添加。具體代碼如下:/// 請求序列化 AFHTTPRequestSerializer *requestSerializer = [AFHTTPRequestSerializer serializer]; /// 配置請求頭 for (NSString *key in parameters) { NSString *value = [[parameters[key] sb_stringValueExtension] copy]; if (value.length==0) continue; /// value只能是字符串,否則崩潰 [requestSerializer setValue:value forHTTPHeaderField:key]; }
最后,注意當我們在使用
AFNetworking
的GET
等方法(API
)時,需要將GET
方法(API
)的參數parameters
,則傳遞的是協議參數
,而不是我們最終得到的參數字典(parameters
)。
方式二:直接將我們最終得到的參數字典(parameters
)傳遞給AFNetworking
的GET
等方法(API
)的參數parameters
即可。MHHTTPRequest主要是通過
MHURLParameters
模型來配置請求模型。以及通過為MHHTTPRequest
創建了分類,能夠在配置完請求模型完成,就可以直接發起MHHTTPService
中的請求。起內容如下:@interface MHHTTPRequest : NSObject /// 請求參數 @property (nonatomic, readonly, strong) MHURLParameters *urlParameters; /** 獲取請求類 @param params 參數模型 @return 請求類 */ +(instancetype)requestWithParameters:(MHURLParameters *)parameters; @end /// MHHTTPService的分類 @interface MHHTTPRequest (MHHTTPService) /// 入隊 - (RACSignal *) enqueueResultClass:(Class /*subclass of MHObject*/) resultClass; @end
MHHTTPService整個網絡服務層(單例),繼承于
AFHTTPSessionManager
,主要用來做網絡數據請求
和用戶數據處理
,這里筆者主要側重將的是其網絡數據請求
。最關鍵的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;
通過執行該方法,我們底層通過
AFNetworking
請求到JSON
數據,并通過YYModel
將JSON
數據的data
字段對應的數據轉化為相應的resultClass
,并最終包裹成MHHTTPResponse
數據,然后通過ReactiveCocoa
轉化成數據信號并返回的過程。當然平常大家在通過網絡請求工具做數據請求時,回調的數據絕大多數都是JSON
數據(id responseObject
),然后在對應的控制器里面做字典轉模型操作。當然筆者這種做法很依賴服務器返回的數據格式,顯然前提是你需要和你的服務端人員共同協商好一份合適數據返回格式,然后再來設計這套網絡工具。首先常用JSON
數據最外層是一個字典,且字段主要是:code
,msg
,data
。code: 請求狀態碼。比如
100:請求成功
,101:對應參數有誤
...
msg: 請求狀態說明,主要是對code
對應的值的解釋。比如請求成功
,點贊成功
...
data請求的數據,且其對應的數據也是一個字典({}
)。YYModel
主要對該字段對應的數據做字典轉模型處理,比如用戶數據
,商品列表
...這里筆者用偽代碼的形式詳述筆者與后臺協商的三種
JSON
數據格式(PS:主要是data
對應的數據變化,以及我們著重需要其內部那些重要數據)。格式一:
data
對應的只是單個字典數據,比如用戶模型
...{ "code" : "100", "msg": "請求成功", "data":{ "user_id" : "100013", "avatar" : "https://...", "nickname": "CodeMikeHe", ... } }
類似這種情況請求數據時,則
resultClass
,傳[MHUser class]
即可。則底層就會通過YYModel
把data
對應的字典轉化成用戶模型(MHUser`)。格式二:
data
對應的是字典且我們只需要該字典的list
對應的數組([]
)列表,比如直播間列表
...{ "code" : "100", "msg": "請求成功", "data":{ "list" : [ {/** 直播間數據 */}, {/** 直播間數據*/}, {/** 直播間數據 */}, {/** 直播間數據 */}, ... ], "totalPage":4, "samecity":0, "hotswitch":null, "hotswitch2":Array[0], "hotConfig":0 ... } }
如果我們只想要獲取
data[@"list"]
對應的數據列表,比如筆者Demo中的首頁數據展示。那么首先我們必須要和后臺人員協商好,后期遇到這種列表(數組)的情況,必須是list
這個字段對應數組列表即可。所以類似這種情況,則resultClass
,傳[MHLive class]
即可。則底層就會通過YYModel
把data
對應list
的列表轉化成直播間模型數組的。當然如果你還想要data
字典中的其他值,那么你就把這個當做 格式一 的方式去處理即可。靈活使用,才是關鍵。格式三:
data
對應的是空值,即<nil>
,比如用戶點贊,一般不需要返回數據,因為我們通過code = 100就可以判斷是否點贊成功,一般這種data是個空值。
...{ "code" : "100", "msg": "點贊成功", "data": <nil>, }
類似這種情況,則
resultClass
傳nil
即可,這樣筆者會原封不動的把后臺的數據返回出去。而你只需要根據code的值來做相應的提示即可。
需要注意的是:resultClass
必須是MHObject
的子類,或者為nil
。否則會Crash
掉。MHHTTPResponse主要是請求成功后返回的服務器數據模型,主要是將服務器最外層的數據(字典),剝離出來而已。其頭文件內容如下:
@interface MHHTTPResponse : MHObject /// The parsed MHObject object corresponding to the API response. /// The developer need care this data 切記:若沒有數據是NSNull 而不是nil .對應于服務器json數據的 data @property (nonatomic, readonly, strong) id parsedResult; /// 自己服務器返回的狀態碼 對應于服務器json數據的 code @property (nonatomic, readonly, assign) MHHTTPResponseCode code; /// 自己服務器返回的信息 對應于服務器json數據的 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
這里的屬性與服務器返回的字段保持一致,只不過用
parsedResult
代替data
罷了。這里需要強調的是,當我們在調用-(RACSignal *)enqueueRequest:(MHHTTPRequest *) request resultClass:(Class /*subclass of MHObject*/) resultClass;
時,resultClass
參數如果我們傳nil
,那么筆者底層將不會利用YYModel
去把data
數據轉化成模型,而是原封不動的服務器的data
數據賦值到parsedResult
。當然,格式一對應的parsedResult
是MHUser
模型;格式二對應的parsedResult
是NSArray <MHLiveRoom *> * parsedResult
模型數組;特別強調的是格式三那種情況,則parsedResult
為NSNull
對象,而不是nil
。這里需要注意的!!!。RACSignal+MHHTTPServiceAdditions主要作用是解析
MHHTTPResponse
數據,通過ReactiveCocoa
的map
方法,將MHHTTPResponse
的parsedResult
映射出來。關鍵代碼如下:- (RACSignal *)mh_parsedResults { return [self map:^(MHHTTPResponse *response) { NSAssert([response isKindOfClass:MHHTTPResponse.class], @"Expected %@ to be an MHHTTPResponse.", response); return response.parsedResult; }]; }
首先開發中我們通過
-(RACSignal *)enqueueRequest:(MHHTTPRequest *) request resultClass:(Class /*subclass of MHObject*/) resultClass;
這個方法返回的一個數據信號resultSignal
,如果訂閱(subscribeNext
)該數據信號resultSignal
其值是MHHTTPResponse
。即偽代碼如下:/// 請求的數據信號 (PS:當然可以一句代碼搞定,這里只做演示) RACSignal *resultSignal = [[MHHTTPService sharedInstance] enqueueRequest:request resultClass:[MHUser class]]; /// 訂閱數據信號 [resultSignal subscribeNext:^(MHHTTPResponse * response) { /// 成功回調 response.parsedResult 為MHUser模型。 } error:^(NSError *error) { /// 失敗回調 } completed:^{ /// 完成 }];
在開發中,我們主要是想要獲取的是
data
對應的數據(PS:即response.parsedResult
的值)。而很少去關注最外層的code
和msg
對應的值。所以,就出現了mh_parsedResults
來直接獲取response.parsedResult
的值。所以上面的偽代碼可以改成以下:[[[[MHHTTPService sharedInstance] enqueueRequest:request resultClass:[MHUser class]] mh_parsedResults] subscribeNext:^(MHUser * user) { /// 成功回調 MHUser模型。 } error:^(NSError *error) { /// 失敗回調 } completed:^{ /// 完成 }];
這樣是不是覺得
高端大氣上檔次,低調奢華有內涵
。當然特別需要注意的是,不是每一個信號(Signal
),都可以調用mh_parsedResults
,必須是訂閱(subscribeNext
)該數據信號resultSignal
其值是MHHTTPResponse
才行,否則程序Crash
。 -
關于使用
這里筆者將喵播
的熱門數據的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為例,講講開發中如何具體使用一下MHHTTPService
。代碼如下:/// 獲取直播間列表 - (RACSignal *)fetchLivesWithUseridx:(NSString *)useridx type:(NSInteger)type page:(NSInteger)page lat:(NSNumber *)lat lon:(NSNumber *)lon province:(NSString *)province{ /// 1. 配置參數 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. 配置參數模型 #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.發起請求 如果你想獲取data的數據而不是data[@"list"]的數據,則resultClass為`[MHLiveInfo class]`即可。 return [[[MHHTTPRequest requestWithParameters:paramters] enqueueResultClass:[MHLiveRoom class]] mh_parsedResults]; }
當然,上面的步驟三(發起請求),其實正常情況下應該為下面的兩步:
/// 配置請求模型 MHHTTPRequest *request = [MHHTTPRequest requestWithParameters:paramters]; /// 發起請求 return [[MHHTTPService sharedInstance] enqueueRequest:request resultClass:[MHLiveRoom class]];
但是由于其過于繁瑣,筆者通過為
MHHTTPRequest
創建了分類,能夠在配置完請求模型完成,就可以直接發起MHHTTPService
中的請求,這樣就優雅的實現了化二為一的效果。關鍵代碼如下:/// 網絡服務層分類 方便MHHTTPRequest 主動發起請求 @implementation MHHTTPRequest (MHHTTPService) /// 請求數據 -(RACSignal *) enqueueResultClass:(Class /*subclass of MHObject*/) resultClass { return [[MHHTTPService sharedInstance] enqueueRequest:self resultClass:resultClass]; } @end
當然,細節注意的地方就是網路請求工具盡量將其作為
MHHTTPService
的分類來設計,且命名要規范,并與請求成功后的模型放在同一個文件夾,這樣更好的提現單一職責化
。比如:請求的用戶數據,分類名稱為:MHHTTPService+User
,主要負責的是: 請求用戶數據, 修改用戶信息 , .... 等API。調試細節注意:在開發過程中,我們可能事先對服務器的返回的數據還一無所知,這樣就無法新建模型,這時候建議先將
resultClass
傳nil
,然后打印數據即可。更多細節還請查看筆者提供的Demo。 -
錯誤處理
請求服務器數據出錯在開發中可謂是家常便飯了,為了提高用戶體驗,我們前端必須處理和解析錯誤信息(NSError
),以便我們更好的根據錯誤信息展示不同的UI以及顯示錯誤提示。當然,相信大部分開發者,都沒怎么好好處理AFNetworking
請求的錯誤信息,都是在AFNetworking
的請求錯誤的block
里面,提示一個服務器不給力,請稍后重試
,網絡有問題,請稍后重試
...等等。當然有提示總比沒提示強,有些開發者根本不處理錯誤信息,頂多是在AFNetworking
的請求錯誤的block
里面NSLog
一下錯誤,這樣用戶體驗極其不佳。當然,前面的提示也是不準確的,或者說不友好。比如有時候是服務器有問題,你卻提示網絡有問題,請稍后重試
;又或者是用戶網絡斷開連接,而你卻提示服務器不給力,請稍后重試
等。當然,我們也沒必要把錯誤信息(NSError
)整個提示出來,這樣也會導致你提示出來的信息可能是一大堆亂七八糟的英文信息,比如錯誤信息如下:{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.}
可能有些錯誤,我們開發者自己都不清楚其原因,你覺得用戶會知道錯誤原因嗎?所以,最主要的是提示準確且簡單的錯誤信息,筆者的做法如下對于
AFNetworking
的錯誤(NSError
),筆者這里只分為三種情況:①服務器請求出錯 ,②請求超時 ,③網路斷開連接。當然在開發人員調試(DEBUG
)狀態下,筆者是會將錯誤碼一同提示出來,方便開發人員準確定位錯誤信息。當然在發布(Release
)狀態下,是不會提示錯誤碼的。關鍵代碼如下所述:#pragma mark - Error Handling /// 請求錯誤解析 - (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,意味著連接不上服務器 NSInteger errorCode = MHHTTPServiceErrorConnectionFailed; NSString *errorDesc = @"服務器出錯了,請稍后重試~"; /// 其實這里需要處理后臺數據錯誤,一般包在 responseObject /// HttpCode錯誤碼解析 https://www.guhei.net/post/jb1153 /// 1xx : 請求消息 [100 102] /// 2xx : 請求成功 [200 206] /// 3xx : 請求重定向[300 307] /// 4xx : 請求錯誤 [400 417] 、[422 426] 、449、451 /// 5xx 、600: 服務器錯誤 [500 510] 、600 NSInteger httpFirstCode = HTTPCode/100; if (httpFirstCode>0) { if (httpFirstCode==4) { /// 請求出錯了,請稍后重試 if (HTTPCode == 408) { #if defined(DEBUG)||defined(_DEBUG) errorDesc = @"請求超時,請稍后再試(408)~"; /// 調試模式 #else errorDesc = @"請求超時,請稍后再試~"; /// 發布模式 #endif }else{ #if defined(DEBUG)||defined(_DEBUG) errorDesc = [NSString stringWithFormat:@"請求出錯了,請稍后重試(%zd)~",HTTPCode]; /// 調試模式 #else errorDesc = @"請求出錯了,請稍后重試~"; /// 發布模式 #endif } }else if (httpFirstCode == 5 || httpFirstCode == 6){ /// 服務器出錯了,請稍后重試 #if defined(DEBUG)||defined(_DEBUG) errorDesc = [NSString stringWithFormat:@"服務器出錯了,請稍后重試(%zd)~",HTTPCode]; /// 調試模式 #else errorDesc = @"服務器出錯了,請稍后重試~"; /// 發布模式 #endif }else if (!self.reachabilityManager.isReachable){ /// 網絡不給力,請檢查網絡 errorDesc = @"網絡開小差了,請稍后重試~"; } }else{ if (!self.reachabilityManager.isReachable){ /// 網絡不給力,請檢查網絡 errorDesc = @"網絡開小差了,請稍后重試~"; } } switch (HTTPCode) { case 400:{ errorCode = MHHTTPServiceErrorBadRequest; /// 請求失敗 break; } case 403:{ errorCode = MHHTTPServiceErrorRequestForbidden; /// 服務器拒絕請求 break; } case 422:{ errorCode = MHHTTPServiceErrorServiceRequestFailed; /// 請求出錯 break; } default: /// 從error中解析 if ([error.domain isEqual:NSURLErrorDomain]) { #if defined(DEBUG)||defined(_DEBUG) errorDesc = [NSString stringWithFormat:@"請求出錯了,請稍后重試(%zd)~",error.code]; /// 調試模式 #else errorDesc = @"請求出錯了,請稍后重試~"; /// 發布模式 #endif switch (error.code) { case NSURLErrorSecureConnectionFailed: case NSURLErrorServerCertificateHasBadDate: case NSURLErrorServerCertificateHasUnknownRoot: case NSURLErrorServerCertificateUntrusted: case NSURLErrorServerCertificateNotYetValid: case NSURLErrorClientCertificateRejected: case NSURLErrorClientCertificateRequired: errorCode = MHHTTPServiceErrorSecureConnectionFailed; /// 建立安全連接出錯了 break; case NSURLErrorTimedOut:{ #if defined(DEBUG)||defined(_DEBUG) errorDesc = @"請求超時,請稍后再試(-1001)~"; /// 調試模式 #else errorDesc = @"請求超時,請稍后再試~"; /// 發布模式 #endif break; } case NSURLErrorNotConnectedToInternet:{ #if defined(DEBUG)||defined(_DEBUG) errorDesc = @"網絡開小差了,請稍后重試(-1009)~"; /// 調試模式 #else errorDesc = @"網絡開小差了,請稍后重試~"; /// 發布模式 #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]; }
當然,還有一種錯誤處理就是利用
AFNetworking
請求數據成功,但是后臺反饋/驗證錯誤信息(msg
)。假設code = 100
為獲取數據成功 , 而其他code ≠ 100
的都是錯誤,且對應錯誤信息msg
字段。這個我們也需要處理,并且也得在調試模式下把code
提示出來,以便后臺開發人員根據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:@"服務器出錯了,請稍后重試(%zd)~",statusCode]; /// 調試模式 #else msgTips = MHStringIsNotEmpty(msgTips)?msgTips:@"服務器出錯了,請稍后重試~"; /// 發布模式 #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]]; }
這樣一來,到時候我們提示錯誤信息就變得
so easy
。例如筆者的在項目中就利用MBProgressHUD
來提示錯誤,當然筆者也為該錯誤(NSError
)的解析提供了分類:關鍵代碼如下:+ (NSString *)mh_tipsFromError:(NSError *)error{ if (!error) return nil; NSString *tipStr = nil; /// 這里需要處理HTTP請求的錯誤 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; }
期待
- 文章若對您有些許幫助,請給個喜歡??,畢竟碼字不易;若對您沒啥幫助,請給點建議??,切記學無止境。
- 針對文章所述內容,閱讀期間任何疑問;請在文章底部評論指出,我會火速解決和修正問題。
- GitHub地址:https://github.com/CoderMikeHe
- 源碼地址:WeChat