iOS重構-輕量級的網絡請求封裝實踐

前言

十分鐘搭建主流框架_簡單的網絡部分(OC)
中,我們使用AFN框架順利的發送網絡請求并返回了有用數據,但對AFN框架的依賴十分嚴重,下面我們重構一下。

源碼github地址

初步

  • 很多時候,我們涉及到網絡請求這塊,都離不開幾個第三方框架,AFNetworkingMJExtention, 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 <AFHTTPSessionManager.h>
      @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 <Foundation/Foundation.h>
#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.使用MJExtensionresult字典數組轉化成CYXMenu模型數組,并返回模型數組
    • 3.外界只需要傳遞進來一個resultClass即可。
  • CYXBaseRequest實現代碼如下:
  • CYXBaseRequest.h文件
#import <Foundation/Foundation.h>

@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 文化拖到您項目中,最好新建一個<Request>文件夾。
  • 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網絡庫。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容

  • AFHTTPRequestOperationManager 網絡傳輸協議UDP、TCP、Http、Socket、X...
    Carden閱讀 4,365評論 0 12
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,142評論 4 61
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,579評論 25 707
  • 公司來了一位新人,之前沒有做過銷售,領導讓他跟著我學習。 開完早會,走出辦公室,我直接就問了起來。 我:“你這個短...
    道心_3433閱讀 245評論 0 0
  • 你沒有發現嗎?我們再也不是那兩個可以徹夜電話的情侶,我們彼此越來越多的是陌生,我們沒有共同的生活圈,也越來越...
    想飛的白兔子閱讀 435評論 0 0