美文网首页iOS学习笔记iOS开发技术IOS开发经验
iOS重构-轻量级的网络请求封装实践

iOS重构-轻量级的网络请求封装实践

作者: Developer_Yancy | 来源:发表于2016-05-31 15:48 被阅读5601次

    前言

    十分钟搭建主流框架_简单的网络部分(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网络库。

    相关文章

      网友评论

      • 请叫我小白同学:大哥,请教一下,我有些疑问,在CYXBaseRequest里面中:有个 NSDictionary *params = [param mj_keyValues]; 模型转字典,还有+ (NSMutableDictionary *)requestParams:(NSDictionary *)dict方法,这两个我不是很懂,在控制器中用参数字典传过去,干嘛要这样呢?AFN内会自动帮我们拼接的吗?还有HttpRequest,为什么不用单列呢?每次请求,都需要创建httprequest实例,岂不是很浪费内存?大哥能否指点一下?
      • PGOne爱吃饺子:楼主 ,可以讲解一下猿题库的YTKNetwork网络库这个框架的知识点么?么么哒
      • SpursGo:这封装的也太轻量级了
      • Scott丶Wang:如果我写一个循环,重复请求一个url,你是怎么处理的?有没有这种处理机制??
        Developer_Yancy:@OneWorld 这样那你加个请求loading的蒙版就能解决了吧
        Scott丶Wang:@Coder_CYX 虽说是异步的,但是有这样情况:界面有一个按钮,点击按钮去进行一个url,这时候我快速点击这个按钮很多次,这时候会对用一个url进行频繁的请求,这时候怎么处理??是不是要标记一下或者判断一下,这样体验是不是会更好一点。如果是一个注册接口,有人恶意大量频繁注册的话,服务器会不会挂掉呢??
        Developer_Yancy:@OneWorld 发送网络请求的回调是异步的。循环请求是什么意思?有这种需求?
      • 指尖猿:我用AFN3.1 拿着demo 中的类过去 直接报错 :sweat:
        指尖猿:@Coder_CYX hud报错 等会给你截图
        Developer_Yancy:@指尖猿 报什么错?
      • wazrx:HUB和菊花等UI操作,我觉得不应该放在网络工具类中处理,转模型的操作更不应该放在网络工具类处理,应该交给相应的model层或者viewModel层进行处理比较好,你这样耦合度太高,不能称之为封装,网络工具类就只应该包含网络请求相关的代码,其实封装的话,除了提供几个请求方法,我觉得更应该提供一些解决常见请求问题的入口,我自己的网络工具类提供了一些属性来快速解决一些常见问题,比如设置请求头,设置contentType,快速解决text/html错误和3840错误、设置超时时间,取消上一次请求等一系列属性,只要设置下属性就能快速解决常见问题,个人建议也可以在网络工具类中提供接口缓存功能,比较实用,个人意见哈~~~
        钱刀为:能否把你的网络请求库 发我一下,1585365122@qq.com。感谢
        钱刀为:能否把你的网络请求库 发我一下,1585365122@qq.com。感谢
        csqingyang:@wazrx 同时,对外提供的接口越简洁越好。分点答题,可以加分的呦~
      • xxttw:不错啊
      • 菜鸟晋升路:接口有问题了 ,网络数据请求不回来
        Developer_Yancy:@菜鸟晋升路 你下载Demo看看,我试过可以的
      • Damonwong:单纯的只是解耦,没必要用单例,因为单例存在内存常驻的问题。建议使用工具类。
        weakSelf 也是没有必要的,你用 AFN 的时候首先不存在循环引用。其次 AFN 他自己也做了弱化处理,最重要的是你如果用 weakSelf 的话,一些后台操作无法执行。例如你进入这个控制器,你需要请求完后把数据存入数据库,如果你用weakSelf 的写法,只要控制器销毁,那么就用无法写入到数据库中。
        封装东西,接口需要的东西越少越好。而不是越封装,需要填的东西越多。
      • 川农鉴黄师:单列的问题,可以用,看你同一时间是否只有一个AF进行网络请求。
      • b1def9fe91c8:谢谢 get
      • csqingyang:几个建议 1 返回给外部使用的数据模型类型的处理逻辑可以放在模型内部进行 2 AFN本身使用 block 时存在的强引用关系最后能在封装时一并解决(弱化)。3 封装的 AFN 中,对用户取消下载任务的操作交给了程序员,可以一并在对 Manager 中进行管理。 4 后续可以简单根据下载 URL 进行单次网络请求结果封装(主要应对有 Session 会话的场景) 5 封装出来的东西,参数尽量精简。
        P.S 二楼说的问题好像是因为没有在封装的文件中使用单例造成的。
        楼主的代码很规范,赞一下。
        飛呈Geek:@清蒸鱼跃龙门 AFN不需要weak处理,它内部已经帮你处理了。
        ```Objective-c
        __weak __typeof(self)weakSelf = self;
        [super setCompletionBlock:^ {
        __strong __typeof(weakSelf)strongSelf = weakSelf;
        ```
        清蒸鱼跃龙门:@csqingyang 2.就是在封装的最外层使用weakself吗,还是控制器里用请求的时候使用?
        Developer_Yancy:@csqingyang 谢谢建议,改天再查查资料优化一下。
      • 十一岁的加重:+ (void)post:(NSString *)url params:(NSDictionary *)params success:(void (^)(id))success failure:(void (^)(NSError *))failure
        {
        // 1.获得请求管理者
        AFHTTPSessionManager *mgr = [AFHTTPSessionManager manager];
        // 2.申明返回的结果是text/html类型


        100个post会创建100个manager
        c26a1985ba8c:@Developer_峰 看源码,管理者并不是单例

      本文标题:iOS重构-轻量级的网络请求封装实践

      本文链接:https://www.haomeiwen.com/subject/afyqlttx.html