授權轉載,作者:Coder_CYX(微博)
前言
在十分鐘搭建主流框架_簡單的網絡部分(OC)中,我們使用AFN框架順利的發送網絡請求並返回了有用數據,但對AFN框架的依賴十分嚴重,下面我們重構一下。
源碼地址:https://github.com/CYXiang/CYXTenMinDemo
初步
很多時候,我們涉及到網絡請求這塊,都離不開幾個第三方框架,AFNetworking,MJExtention, MBProgressHUD(SV)。
初學的時候,都會把它們寫到Controller裡面,如下:
[[AFHTTPSessionManager manager] GET:CYXRequestURL parameters:params success:^(NSURLSessionDataTask * _Nonnull task, id _Nonnull responseObject) { NSLog(@"請求成功"); // 利用MJExtension框架進行字典轉模型 weakSelf.menus = [CYXMenu objectArrayWithKeyValuesArray:responseObject[@"result"]]; // 刷新數據(若不刷新數據會顯示不出) [weakSelf.tableView reloadData]; } failure:^(NSURLSessionDataTask * _Nonnull task, NSError * _Nonnull error) { NSLog(@"請求失敗 原因:%@",error); }];
這樣會造成耦合性過高的問題,靈活性也非常不好,因此,AFN的作者也推薦我們不要直接使用,新建一個網絡請求類來繼承AFN的使用方式更好。
因此,繼承的方式,如下:
CYXHTTPSessionManager.h文件 #import @interface CYXHTTPSessionManager : AFHTTPSessionManager @end CYXHTTPSessionManager.m文件 #import "CYXHTTPSessionManager.h" @implementation CYXHTTPSessionManager + (instancetype)manager{ CYXHTTPSessionManager *mgr = [super manager]; // 這裡可以做一些統一的配置 // mgr.responseSerializer = ; // mgr.requestSerializer = ; return mgr; } @end
調用方式:
/** 請求管理者 */ @property (nonatomic,weak) CYXHTTPSessionManager * manager; // 發送請求 [self.manager GET:CYXRequestURL parameters:params success:^(NSURLSessionDataTask * _Nonnull task, id _Nonnull responseObject) { // 存儲 maxtime weakSelf.maxtime = responseObject[@"info"][@"maxtime"]; weakSelf.topics = [CYXTopic objectArrayWithKeyValuesArray:responseObject[@"list"]]; CYXLog(@"%@",responseObject[@"list"]); [weakSelf.tableView reloadData]; // 結束刷新 [weakSelf.tableView.header endRefreshing]; } failure:^(NSURLSessionDataTask * _Nonnull task, NSError * _Nonnull error) { [weakSelf.tableView.header endRefreshing]; }];
這樣,已經降低了一點耦合度,也不需要在每個需要發送網絡請求的Controller中引入AFN框架了。但對於MJExtension框架的依賴還是沒有改善。
進階
通過觀察,我們發現其實大部分的GET和POST請求的前幾步基本使用步驟是大致相同的,相同的步驟如下:
1.通過AFN請求回來JSON數據
2.通過JSON數據,取出需要使用的字典數組/字典
3.使用字典轉模型框架(MJExtension)把字典數組轉化為模型數組/字典轉化為模型
因此,我們思考能不能把這些相同的步驟封裝起來,以後就不需要重復寫這些代碼了,我們都知道一條經典的編程法則:“Don't repeat youself”。這就是我們封裝與重構的理由!
1.基層請求的封裝
本文示例封裝POST請求
CYXHttpRequest.h文件
#import #import "AFNetworking.h" @interface CYXHttpRequest : NSObject /** * 發送一個POST請求 * * @param url 請求路徑 * @param params 請求參數 * @param success 請求成功後的回調 * @param failure 請求失敗後的回調 */ + (void)post:(NSString *)url params:(NSDictionary *)params success:(void (^)(id responseObj))success failure:(void (^)(NSError *error))failure; @end
CYXHttpRequest.m文件
#import "CYXHttpRequest.h" @implementation CYXHttpRequest + (void)post:(NSString *)url params:(NSDictionary *)params success:(void (^)(id))success failure:(void (^)(NSError *))failure { // 1.獲得請求管理者 AFHTTPSessionManager *mgr = [AFHTTPSessionManager manager]; // 2.申明返回的結果是text/html類型 mgr.responseSerializer = [AFHTTPResponseSerializer serializer]; // 3.設置超時時間為10s mgr.requestSerializer.timeoutInterval = 10; // 4.發送POST請求 [mgr POST:url parameters:params progress:^(NSProgress * _Nonnull uploadProgress) { } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { if (success) { success(responseObject); } } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { if (failure) { failure(error); } }]; } @end
現在已經可以把網絡數據請求回來了,輪到第二個步驟了:觀察請求回來的JSON數據,取出需要使用的字典數組/字典。在這裡再作一層封裝。舉個簡單的例子,假如返回的JSON數據結構如下:
{ "error_code": 0, "reason": "Success", "result": [{ "id": 370622, "title": "西紅柿蒜薹炒雞蛋", "tags": "廚房用具;廚具;加工工藝;基本工藝;菜品;菜肴;家常菜;炒;炒鍋;熱菜;防輻射;開胃;蔬菜類;果實類;蒜薹;西紅柿;禽蛋類;蛋;雞蛋;", "intro": "我這的蒜薹雞蛋都愛加西紅柿、辣椒一起炒的,這是習慣所致,愛吃西紅柿,愛吃辣椒,還愛把菜搭配的顏色亮麗,當然味道也不差。", "ingredients": "西紅柿:1個;蒜薹:200g;雞蛋:2個;", "burden": "油:適量;鹽:適量;青辣椒:1個;紅辣椒:1個;", "albums": "http://imgs.haoservice.com/CaiPu/pic/recipe/l/be/a7/370622_86e12b.jpg", } { "id": 433079, "title": "西紅柿酸奶", "tags": "促進食欲;減肥;懶人食譜;消暑食譜;美容養顏;", "intro": "新疆人愛吃西紅柿那是有目共睹的,菜裡面加西紅柿的數不勝數,就連舌尖2在吐魯番拍的葡萄干抓飯裡面都加西紅柿。", "ingredients": "酸奶:400g;西紅柿:200g;", "burden": "白糖:20g;", "albums": "http://imgs.haoservice.com/CaiPu/pic/recipe/l/b7/9b/433079_377373.jpg", } {···}] }
2.簡單業務邏輯封裝
現在只需要使用到result數據(並對應CYXMenu模型),在公司中,接口一般會有比較好的規范,即每個接口的模型屬性一般都有統一的命名。
我們使用時,通常會把result字典數組轉化成CYXMenu模型數組。因此,可以進一步的封裝出CYXBaseRequest對象。
CYXBaseRequest類實現思路如下:
1)使用CYXHttpRequest發起網絡請求,返回數據中取到result
2)使用MJExtension將result字典數組轉化成CYXMenu模型數組,並返回模型數組
3)外界只需要傳遞進來一個resultClass即可。
CYXBaseRequest實現代碼如下:
CYXBaseRequest.h文件
#import @interface CYXBaseRequest : NSObject /** * 返回result 數據模型 * * @param url 請求地址 * @param param 請求參數 * @param resultClass 需要轉換返回的數據模型 * @param success 請求成功後的回調 * @param warn 請求失敗後警告提示語 * @param failure 請求失敗後的回調 * @param tokenInvalid token過期後的回調 */ + (void)postResultWithUrl:(NSString *)url param:(id)param resultClass:(Class)resultClass success:(void (^)(id result))success warn:(void (^)(NSString *warnMsg))warn failure:(void (^)(NSError *error))failure tokenInvalid:(void (^)())tokenInvalid; /** * 返回result 數據模型(帶HUD) * * @param url 請求地址 * @param param 請求參數 * @param resultClass 需要轉換返回的數據模型 * @param success 請求成功後的回調 * @param warn 請求失敗後警告提示語 * @param failure 請求失敗後的回調 * @param tokenInvalid token過期後的回調 */ + (void)postResultHUDWithUrl:(NSString *)url param:(id)param resultClass:(Class)resultClass success:(void (^)(id result))success warn:(void (^)(NSString *warnMsg))warn failure:(void (^)(NSError *error))failure tokenInvalid:(void (^)())tokenInvalid; /** * 組合請求參數 * * @param dict 外部參數字典 * * @return 返回組合參數 */ + (NSMutableDictionary *)requestParams:(NSDictionary *)dict; @end
CYXBaseRequest.m文件
#import "CYXBaseRequest.h" #import "CYXHttpRequest.h" #import "ExceptionMsgTips.h" #import "MJExtension.h" @implementation BSBaseRequest /** * 返回result 數據模型(HUD) */ + (void)postResultHUDWithUrl:(NSString *)url param:(id)param resultClass:(Class)resultClass success:(void (^)(id result))success warn:(void (^)(NSString *warnMsg))warn failure:(void (^)(NSError *error))failure tokenInvalid:(void (^)())tokenInvalid { [self postBaseHUDWithUrl:url param:param resultClass:resultClass success:^(id responseObj) { if (!resultClass) { success(nil); return; } success([resultClass mj_objectArrayWithKeyValuesArray:responseObj[@"result"]]); } warn:warn failure:failure tokenInvalid:tokenInvalid]; } /** * 返回result 數據模型 */ + (void)postResultWithUrl:(NSString *)url param:(id)param resultClass:(Class)resultClass success:(void (^)(id result))success warn:(void (^)(NSString *warnMsg))warn failure:(void (^)(NSError *error))failure tokenInvalid:(void (^)())tokenInvalid { [self postBaseWithUrl:url param:param resultClass:resultClass success:^(id responseObj) { if (!resultClass) { success(nil); return; } success([resultClass mj_objectArrayWithKeyValuesArray:responseObj[@"result"]]); } warn:warn failure:failure tokenInvalid:tokenInvalid]; } /** * 數據模型基類方法 */ + (void)postBaseWithUrl:(NSString *)url param:(id)param resultClass:(Class)resultClass success:(void (^)(id result))success warn:(void (^)(NSString *warnMsg))warn failure:(void (^)(NSError *error))failure tokenInvalid:(void (^)())tokenInvalid { // url = [NSString stringWithFormat:@"%@%@",Host,url]; CYXLog(@"\\n請求鏈接地址---> %@",url); //狀態欄菊花 [UIApplication sharedApplication].networkActivityIndicatorVisible = YES; [CYXHttpRequest post:url params:param success:^(id responseObj) { if (success) { NSDictionary *dictData = [NSJSONSerialization JSONObjectWithData:responseObj options:kNilOptions error:nil]; CYXLog(@"請求成功,返回數據 : %@",dictData); success(dictData); } //狀態欄菊花 [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; } failure:^(NSError *error) { if (failure) { failure(error); CYXLog(@"請求失敗:%@",error); } //狀態欄菊花 [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; }]; } /** * 數據模型基類(帶HUD) */ + (void)postBaseHUDWithUrl:(NSString *)url param:(id)param resultClass:(Class)resultClass success:(void (^)(id result))success warn:(void (^)(NSString *warnMsg))warn failure:(void (^)(NSError *error))failure tokenInvalid:(void (^)())tokenInvalid { [SVProgressHUD showWithStatus:@""]; [self postBaseWithUrl:url param:param resultClass:resultClass success:^(id responseObj) { [SVProgressHUD dismiss]; //隱藏loading success(responseObj); } warn:^(NSString *warnMsg) { [SVProgressHUD dismiss]; warn(warnMsg); } failure:^(NSError *fail) { [SVProgressHUD dismiss]; failure(fail); } tokenInvalid:^{ [SVProgressHUD dismiss]; tokenInvalid(); }]; } @end
到這裡,輕量級的封裝介紹已經全部介紹完了,更多的功能封裝有待讀者自己去研究了。既然封裝好了,下面我們來介紹一下如何使用,其實非常簡單。
使用介紹
1.把上述兩個類的.h .m 文化拖到您項目中,最好新建一個
2.在需要發送請求的Controller中#import "CYXBaseRequest.h"
3.發送請求方法中的代碼如下:
(使用CYXBaseRequest):
#pragma mark - 請求數據方法 - (void)loadData{ self.pn = 1; // 請求參數(根據接口文檔編寫) NSMutableDictionary *params = [NSMutableDictionary dictionary]; params[@"menu"] = @"西紅柿"; params[@"pn"] = @(self.pn); params[@"rn"] = @"10"; params[@"key"] = @"fcfdb87c50c1485e9e7fa9f839c4b1a8"; [CYXBaseRequest postResultWithUrl:CYXRequestURL param:params resultClass:[CYXMenu class] success:^(id result) { CYXLog(@"請求成功,返回數據 : %@",result); self.menus = result; self.pn ++; // 刷新數據(若不刷新數據會顯示不出) [self.tableView reloadData]; [self.tableView.mj_header endRefreshing]; } warn:^(NSString *warnMsg) { } failure:^(NSError *error) { CYXLog(@"請求失敗 原因:%@",error); [self.tableView.mj_header endRefreshing]; } tokenInvalid:^{ // 有登錄操作的業務,這裡返回登錄狀態 }]; }
在這裡對比一下不使用CYXBaseRequest的發送請求方法代碼:
#pragma mark - 請求數據方法 - (void)loadData{ self.pn = 1; // 請求參數(根據接口文檔編寫) NSMutableDictionary *params = [NSMutableDictionary dictionary]; params[@"menu"] = @"西紅柿"; params[@"pn"] = @(self.pn); params[@"rn"] = @"10"; params[@"key"] = @"fcfdb87c50c1485e9e7fa9f839c4b1a8"; [self.manager.tasks makeObjectsPerformSelector:@selector(cancel)]; [self.manager.responseSerializer setAcceptableContentTypes:[NSSet setWithObject:@"text/html"]]; [self.manager POST:CYXRequestURL parameters:params progress:^(NSProgress * _Nonnull downloadProgress) { } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { CYXLog(@"請求成功,返回數據 : %@",responseObject); // 利用MJExtension框架進行字典轉模型 weakSelf.menus = [CYXMenu mj_objectArrayWithKeyValuesArray:responseObject[@"result"]]; weakSelf.pn ++; // 刷新數據(若不刷新數據會顯示不出) [weakSelf.tableView reloadData]; [weakSelf.tableView.mj_header endRefreshing]; } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { CYXLog(@"請求失敗 原因:%@",error); [weakSelf.tableView.mj_header endRefreshing]; }]; }
雖然從代碼看似兩種使用差別不太大(只是少了幾行代碼),但相比之下,前者確實降低了對AFN等框架的依賴,並省去了每次都手動轉一下模型的煩惱,現在你只需要把resultClass傳過去,返回的數據便是已經轉化好的模型,並在CYXBaseRequest內打印出請求鏈接地址,返回數據等有用信息,方便調試,接口設計也類似AFN,使用簡便。
TIPS:建議使用者可以在每個模塊都建立Request文件(繼承CYXBaseRequest),統一進行網絡請求,這樣更方便管理。
注:
本封裝實踐只對網絡請求進行初步的簡單封裝,僅適用於中小型的項目,並不涉及緩存、校驗等高級功能,如果有高級需求,建議研究下猿題庫的YTKNetwork網絡庫。
附:源碼Github地址。