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

2020-01-31 01:23栏目:龙竞技官网
TAG:

通过curl对接口发起post请求的时候很少会遇到请求数据是二维数组的情况,一般情况下只需要按照正常的方式发送请求就可以了,可能的代码如下:

Demo1

在十分钟搭建主流框架_龙电竞官网,简单的网络部分中,我们使用AFN框架顺利的发送网络请求并返回了有用数据,但对AFN框架的依赖十分严重,下面我们重构一下。

$ch = curl_init();curl_setopt($ch, CURLOPT_URL, $url);curl_setopt($ch, CURLOPT_HEADER, FALSE);curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);curl_setopt($ch, CURLOPT_POST, 1);curl_setopt($ch, CURLOPT_POSTFIELDS, $data);$result = curl_exec;if  { curl_close; $res_data = json_decode($result, true); echo '<pre>'; print_r($res_data); echo '</pre>';} else { $error = curl_errno; echo 'curl出错,错误码('.$error.')'; curl_close;}
#import "ViewController.h"
#import "Status.h"
#import "StatusCell.h"
#import "MJExtension.h"
#import "MJRefresh/MJRefresh.h"
#import "AFNetworking.h"
#import "SVProgressHUD.h"

@interface ViewController ()
/** 所有的微博模型*/
@property (nonatomic ,strong) NSMutableArray *statuses;
@property(nonatomic,assign) NSInteger page;

@end

@implementation ViewController

- (BOOL)prefersStatusBarHidden{
    return YES;
}

- (NSMutableArray *)statuses
{
    if (!_statuses) {
        _statuses = [NSMutableArray array];
    }
    return _statuses;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    // self-sizing(iOS8 以后)
    // 告诉tableView所有cell的高度是自动计算的(根据设置的约束来计算)
    self.tableView.rowHeight = UITableViewAutomaticDimension;

    // 告诉tableView所有cell的估算高度
    self.tableView.estimatedRowHeight = 44;

    __weak __typeof(self) weakSelf = self;
    // 设置回调(一旦进入刷新状态就会调用这个refreshingBlock)
    self.tableView.header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
        // 将发送网络请求的方法写在这个代码块里。以后只要我们滚动tableView就会自动执行这个代码块。因为MJ在底层已经为我们实现了:如果我们滚动tableView,就会触发headerWithRefreshingBlock,执行代码块中的内容
        [weakSelf getDataWithHeader:YES];
    }];

    self.tableView.footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{
         // 将发送网络请求的方法写在这个代码块里。以后只要我们滚动tableView就会自动执行这个代码块。因为MJ在底层已经为我们实现了:如果我们滚动tableView,就会触发headerWithRefreshingBlock,执行代码块中的内容
        [weakSelf getDataWithHeader:NO];
    }];

    // 自动进入刷新状态,执行headerWithRefreshingBlock中的代码块.
    // 相当于你手动下拉然后松手,就会执行headerWithRefreshingBlock中的代码块{}。
    // 如果没有这句代码,程序初始运行将不会执行代买块中的内容,所以也就不会发送网络请求,所以tableView上啥也不显示
    [self.tableView.header beginRefreshing];
}

-(void)getDataWithHeader:(BOOL)status
{
    // 如果是下拉刷新就设置当前页数为1,否则每次都+1
    if (status) {
        self.page = 1;
        // 下拉刷新清空以前的数据
        [self.statuses removeAllObjects];
    }else{
        self.page += 1;
    }
    // 创建AFN管理者
    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
    manager.requestSerializer = [AFJSONRequestSerializer serializer];

    // 设置参数
    NSDictionary *params = @{@"page":@(self.page)};
    // 设置URL
    NSString *baseUrl = @"http://api.liyaogang.com/weibo/status.php";
    // 发送请求
    [manager POST:baseUrl parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
        // 解析数据
        NSMutableArray *arr = [Status mj_objectArrayWithKeyValuesArray:responseObject];
        [self.statuses addObjectsFromArray:arr];

        // 刷新表格数据
        if (arr.count != 0) {
            [self.tableView reloadData];
        }else{
            // 设置黑色的遮罩
            [SVProgressHUD setDefaultMaskType:SVProgressHUDMaskTypeBlack];
            [SVProgressHUD showErrorWithStatus:@"没有更多数据了"];
        }
        // 拿到当前的下拉刷新控件,结束刷新状态
        [self.tableView.header endRefreshing];
        [self.tableView.footer endRefreshing];

    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        // 拿到当前的下拉刷新控件,结束刷新状态
        [self.tableView.header endRefreshing];
        [self.tableView.footer endRefreshing];
        NSLog(@"%@",error);
    }];
}

#pragma mark - 数据源方法
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.statuses.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *ID = @"status";
    StatusCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];

    // 传递模型数据
    cell.status = self.statuses[indexPath.row];
    return cell;
}

@end
源码github地址
  • 很多时候,我们涉及到网络请求这块,都离不开几个第三方框架,AFNetworkingMJExtention, MBProgressHUD
  • 初学的时候,都会把它们写到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”。这就是我们封装与重构的理由!

但是当请求的数据是$data是二维数组的时候,php就会提示Array to string conversion,这个时候需要使用函数http_build_query()来处理$data,调整后的代码为:

  • 未调用beginRefreshing,程序初始运行并不会自动发送网络请求

1.基层请求的封装

  • 本文示例封装POST请求
  • CYXHttpRequest.h文件
#import <Foundation/Foundation.h>#import "AFNetworking.h"@interface CYXHttpRequest : NSObject/** * 发送一个POST请求 * * @param url 请求路径 * @param params 请求参数 * @param success 请求成功后的回调 * @param failure 请求失败后的回调 */+ post:(NSString *)url params:(NSDictionary *)params success:(id responseObj))success failure:(NSError *error))failure;@end
  • CYXHttpRequest.m文件
#import "CYXHttpRequest.h"@implementation CYXHttpRequest+ post:(NSString *)url params:(NSDictionary *)params success:success failure:(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(responseObject); } } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { if  { failure; } }];}@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", } {···}]}
$ch = curl_init();curl_setopt($ch, CURLOPT_URL, $url);curl_setopt($ch, CURLOPT_HEADER, FALSE);curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);curl_setopt($ch, CURLOPT_POST, 1);curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query;$result = curl_exec;if  { curl_close; $res_data = json_decode($result, true); echo '<pre>'; print_r($res_data); echo '</pre>';} else { $error = curl_errno; echo 'curl出错,错误码('.$error.')'; curl_close;}

龙电竞官网 1

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过期后的回调 */+ postResultWithUrl:(NSString *)url param:param resultClass:resultClass success:(id result))success warn:(NSString *warnMsg))warn failure:(NSError *error))failure tokenInvalid:tokenInvalid;/** * 返回result 数据模型 * * @param url 请求地址 * @param param 请求参数 * @param resultClass 需要转换返回的数据模型 * @param success 请求成功后的回调 * @param warn 请求失败后警告提示语 * @param failure 请求失败后的回调 * @param tokenInvalid token过期后的回调 */+ postResultHUDWithUrl:(NSString *)url param:param resultClass:resultClass success:(id result))success warn:(NSString *warnMsg))warn failure:(NSError *error))failure tokenInvalid: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 数据模型 */+ postResultHUDWithUrl:(NSString *)url param:param resultClass:resultClass success:(id result))success warn:(NSString *warnMsg))warn failure:(NSError *error))failure tokenInvalid:tokenInvalid{ [self postBaseHUDWithUrl:url param:param resultClass:resultClass success:^(id responseObj) { if (!resultClass) { success; return; } success([resultClass mj_objectArrayWithKeyValuesArray:responseObj[@"result"]]); } warn:warn failure:failure tokenInvalid:tokenInvalid];}/** * 返回result 数据模型 */+ postResultWithUrl:(NSString *)url param:param resultClass:resultClass success:(id result))success warn:(NSString *warnMsg))warn failure:(NSError *error))failure tokenInvalid:tokenInvalid{ [self postBaseWithUrl:url param:param resultClass:resultClass success:^(id responseObj) { if (!resultClass) { success; return; } success([resultClass mj_objectArrayWithKeyValuesArray:responseObj[@"result"]]); } warn:warn failure:failure tokenInvalid:tokenInvalid];}/** * 数据模型基类方法 */+ postBaseWithUrl:(NSString *)url param:param resultClass:resultClass success:(id result))success warn:(NSString *warnMsg))warn failure:(NSError *error))failure tokenInvalid:tokenInvalid{// url = [NSString stringWithFormat:@"%@%@",Host,url]; CYXLog(@"\n请求链接地址---> %@",url); //状态栏菊花 [UIApplication sharedApplication].networkActivityIndicatorVisible = YES; [CYXHttpRequest post:url params:param success:^(id responseObj) { if  { NSDictionary *dictData = [NSJSONSerialization JSONObjectWithData:responseObj options:kNilOptions error:nil]; CYXLog(@"请求成功,返回数据 : %@",dictData); success; } //状态栏菊花 [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; } failure:^(NSError *error) { if  { failure; CYXLog(@"请求失败:%@",error); } //状态栏菊花 [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; }];}/** * 数据模型基类 */+ postBaseHUDWithUrl:(NSString *)url param:param resultClass:resultClass success:(id result))success warn:(NSString *warnMsg))warn failure:(NSError *error))failure tokenInvalid:tokenInvalid{ [SVProgressHUD showWithStatus:@""]; [self postBaseWithUrl:url param:param resultClass:resultClass success:^(id responseObj) { [SVProgressHUD dismiss]; //隐藏loading success(responseObj); } warn:^(NSString *warnMsg) { [SVProgressHUD dismiss]; warn; } failure:^(NSError *fail) { [SVProgressHUD dismiss]; failure; } tokenInvalid:^{ [SVProgressHUD dismiss]; tokenInvalid(); }];}@end
  • 到这里,轻量级的封装介绍已经全部介绍完了,更多的功能封装有待读者自己去研究了。既然封装好了,下面我们来介绍一下如何使用,其实非常简单。

这样提示信息就不会再出现了。

没有beginRefreshing.gif

使用介绍

  • 1.把上述两个类的.h .m 文化拖到您项目中,最好新建一个<Request>文件夹。
  • 2.在需要发送请求的Controller中#import "CYXBaseRequest.h"
  • 3.发送请求方法中的代码如下:
    • (使用CYXBaseRequest):
#pragma mark - 请求数据方法- loadData{ self.pn = 1; // 请求参数 NSMutableDictionary *params = [NSMutableDictionary dictionary]; params[@"menu"] = @"西红柿"; params[@"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 - 请求数据方法- loadData{ self.pn = 1; // 请求参数 NSMutableDictionary *params = [NSMutableDictionary dictionary]; params[@"menu"] = @"西红柿"; params[@"pn"] = @; params[@"rn"] = @"10"; params[@"key"] = @"fcfdb87c50c1485e9e7fa9f839c4b1a8"; [self.manager.tasks makeObjectsPerformSelector:@selector]; [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地址
  • 未调用beginRefreshing,程序初始运行会自动发送网络请求
![](https://upload-images.jianshu.io/upload_images/2364940-5a0f35a44140b77c.gif)

有beginRefreshing.gif

链接密码

版权声明:本文由龙竞技官网发布于龙竞技官网,转载请注明出处:iOS重构-轻量级的网络请求封装实践