前言
這篇文章是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)用AFNetworking
的GET/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í),筆者主要參照OctoKit的API
來開發(fā)設(shè)計(jì)的,以及數(shù)據(jù)請(qǐng)求和數(shù)據(jù)回調(diào)信號(hào)(RACSignal
)的內(nèi)部實(shí)現(xiàn)筆者主要參考的是AFNetworking-RACExtensions和OctoKit的實(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)
-
文件說明
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ù)器的privatekey
和privateValue
(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ù)
的鍵值,以及最后增加的sign
和signValue
。參考代碼如下:#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
)的key
和value
的添加。具體代碼如下:/// 請(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
)傳遞給AFNetworking
的GET
等方法(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ù),并通過YYModel
將JSON
數(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è)字典,且字段主要是:code
,msg
,data
。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ì)通過YYModel
把data
對(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ì)通過YYModel
把data
對(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>, }
類似這種情況,則
resultClass
傳nil
即可,這樣筆者會(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)的parsedResult
是MHUser
模型;格式二對(duì)應(yīng)的parsedResult
是NSArray <MHLiveRoom *> * parsedResult
模型數(shù)組;特別強(qiáng)調(diào)的是格式三那種情況,則parsedResult
為NSNull
對(duì)象,而不是nil
。這里需要注意的!!!。RACSignal+MHHTTPServiceAdditions主要作用是解析
MHHTTPResponse
數(shù)據(jù),通過ReactiveCocoa
的map
方法,將MHHTTPResponse
的parsedResult
映射出來。關(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)注最外層的code
和msg
對(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í)候建議先將
resultClass
傳nil
,然后打印數(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; }
期待
- 文章若對(duì)您有些許幫助,請(qǐng)給個(gè)喜歡??,畢竟碼字不易;若對(duì)您沒啥幫助,請(qǐng)給點(diǎn)建議??,切記學(xué)無止境。
- 針對(duì)文章所述內(nèi)容,閱讀期間任何疑問;請(qǐng)?jiān)谖恼碌撞吭u(píng)論指出,我會(huì)火速解決和修正問題。
- GitHub地址:https://github.com/CoderMikeHe
- 源碼地址:WeChat