网络请求--OC对AFN的封装,Swift对Alamofire的封装

背景

因为对Swift的热爱和追求,我们公司项目采取了OC和Swift混编的模式,虽然后面发现这样会带来挺多坑的但是我们还是乐此不彼。

在这里谈谈网络这块,由于网络请求一直用的是前辈(上一位员工)封装好的工具类(坑)。基本的使用没啥问题,但是使用起来太麻烦了,每次发一个请求都要创建一堆的参数,而且对返回结果没有统一处理的方法,就直接抛给请求者,这样会在请求的地方要写一堆的代码去判断这次请求是否为有效的。而且最为重要的是,我发现当我在Swift上使用这套方法的时候,出现各种问题。所以在用的很不爽的情况下,自己重新搞了一套请了请求的工具类。

alamofire.jpeg

改进

  • 使用YJBaseRequest和YJBaseTableRequest两个基类统一管理网络请求需要的参数(如果需要传递参数,继承这两个基类即可);
  • 使用两套请求方案,在请求上OC使用AFN,Swift使用Alamofire;在返回结果上首先会调用一个统一过滤返回结果的方法,最后才会把结果抛出来,这样其他人在调用的时候,只需要简单的判断这次请求是否有效的,做相应的处理就行了。
  • 在返回数据方面:OC我会返回id类型的响应值,相当于不做处理,在请求端选择相应的JSON转模型的框架。但是在Swift方面,我会直接使用一个我非常非常喜欢的框架SwiftyJSON,给调用者直接返回一个json格式的对象,这样在调用者根本不用怕,因没有获取到相应的值而做各种守护和判断的问题。

涉及的第三方框架

AFNetworking

Alamofire

SwiftyJSON

MJExtension

SVProgressHUD

请求类型

  • 因为项目是一个理财类的app,基本涉及不到DELETE和PUT等其他类型的网络请求,所以这个版本只支持getpost请求。

请求配置

  • 这里我采取对象转JSON的方法,我首先封装好两个请求基类YJBaseRequestYJBaseTableRequest,里面包含请求路径和分页参数等等。在使用者需要发请求时,如果需要添加其他参数,首先要集成这两个基类中的一个即可,在发请求时,我会把传过来的对象转成对应的请求参数。

请求返回值

很多时候我们封装框架,会把错误信息一并返回给请求者,其实我觉得这个没有太多的必要,特别是对于一些像未登录而提示的错误,而且要去做相应操作的。我们完全可以在请求完成后统一去过滤和处理就可以了,调用者只需要知道,这次请求是否成功,返回数据是什么就行了。

所以我的返回Block只有两个值,一个代表这次请求是否成功的参数success,另一个是返回数据OC是id类型的responseObject,Swift是SwiftyJSON类型的JSON数据。

请求方法

为了让外界使用方便,我提供了一个带请求参数和不带请求参数的方法。

Swift

/**
       Get方式的网络请求
     * request : 网络请求配置(路径、参数等等)
     * RequestCompletion : 网络回调
     *      success : ture表示此次网络请求成功, 否则失败(false)
     *      JSON    : 返回JSON结构体的数据,有可能为nil
     */
    func get(request: YJBaseRequest, completion: @escaping RequestCompletion) -> () {
        self.printRequestInfo(.get, request)
        self.request(.get, request, completion)
    }
    
    /**
       POST方式的网络请求
     * request : 网络请求配置(路径、参数等等)
     * RequestCompletion : 网络回调
     *      success : ture表示此次网络请求成功, 否则失败(false)
     *      JSON    : 返回JSON结构体的数据,有可能为nil
     */
    func post(request: YJBaseRequest, completion: @escaping RequestCompletion) -> () {
        self.printRequestInfo(.post, request)
        self.request(.post, request, completion)
    }
    
    func get(urlString: String?, completion: @escaping RequestCompletion) -> () {
        let request: YJBaseRequest = YJBaseRequest.init(urlString: urlString ?? "")
        self.printRequestInfo(.get, request)
        self.request(.get, request, completion)
    }
    
    func post(urlString: String?, completion: @escaping RequestCompletion) -> () {
        let request: YJBaseRequest = YJBaseRequest.init(urlString: urlString ?? "")
        self.printRequestInfo(.post, request)
        self.request(.post, request, completion)
    }

其中printRequestInfo方法是在DEBUG模式下,用来打印请求数据的,方便调试。

OC

/**
 Get方式的网络请求
 * request : 网络请求配置(路径、参数等等)
 * CompletionHandler : 网络回调
 *      success : ture表示此次网络请求成功, 否则失败(false)
 *      responseObject : 返回id类型的数据
 */
- (void)get:(YJBaseRequest *)request completion:(CompletionHandler)completion {
    [self logRequestInfoWithType:YJRequestTypeGet request:request];
    [self requestWithType:YJRequestTypeGet request:request completion:completion];
}

/**
 Post方式的网络请求
 * request : 网络请求配置(路径、参数等等)
 * RequestCompletion : 网络回调
 *      success : ture表示此次网络请求成功, 否则失败(false)
 *      responseObject : 返回id类型的数据
 */
- (void)post:(YJBaseRequest *)request completion:(CompletionHandler)completion {
    [self logRequestInfoWithType:YJRequestTypePost request:request];
    [self requestWithType:YJRequestTypePost request:request completion:completion];
}

- (void)getWithUrlString:(NSString *)urlString completion:(CompletionHandler)completion {
    YJBaseRequest *request = [[YJBaseRequest alloc] initWithUrlString:urlString];
    [self logRequestInfoWithType:YJRequestTypeGet request:request];
    [self requestWithType:YJRequestTypeGet request:request completion:completion];
}

- (void)postWithUrlString:(NSString *)urlString completion:(CompletionHandler)completion {
    YJBaseRequest *request = [[YJBaseRequest alloc] initWithUrlString:urlString];
    [self logRequestInfoWithType:YJRequestTypePost request:request];
    [self requestWithType:YJRequestTypePost request:request completion:completion];
}

其中logRequestInfoWithType:request:方法是在DEBUG模式下,用来打印请求数据的,方便调试。

网络请求

Swift

// MARK: - <Alamofire>
    private func request(_ type: RequestType, _ request: YJBaseRequest, _ completion: @escaping RequestCompletion) -> () {
        guard request.urlString.characters.count > 0 else {
            #if DEBUGSWIFT
                SVProgressHUD.showInfo(withStatus: "兄弟,你没有设置请求路径。")
            #endif
            completion(false, nil)
            return
        }
        let urlStr: String = String.init(format: "%@%@", YJBaseURLString, request.urlString)

        weak var weakSelf = self
        let method: HTTPMethod = (type == .get ? HTTPMethod.get : HTTPMethod.post)
        let params: Dictionary<String, Any> = request.parameters as! Dictionary<String, Any>
        Alamofire.request(urlStr, method: method, parameters: params, headers: self.httpHeaders()).responseJSON(completionHandler: { (response) in
            
            switch response.result {
            case .success:
                if let value = response.result.value as? [String: AnyObject] {
                    weakSelf?.handleResponseResults(value, response.response, nil, completion)
                } else {
                    weakSelf?.handleResponseResults(nil, response.response, nil, completion)
                }
            case .failure(let error):
                weakSelf?.handleResponseResults(nil, response.response, error, completion)
            }
        })
    }
    
    // MARK: - 统一处理网络请求返回值
    private func handleResponseResults(_ responseObject: Any?, _ httpResponse: HTTPURLResponse?, _ error: Error?, _ completion: @escaping RequestCompletion) -> () {
        YJRequestManager.share().handleResponseResults(withResponse: responseObject, httpResponse: httpResponse, error: error) { (success, responseObject) in
            guard success, responseObject != nil else {
                completion(success, nil)
                return
            }
            let json = JSON(responseObject!)
            completion(true, json)
        }
    }

这里Swift调用了OC封装的统一过滤网络请求的方法。

  • 其它:
    // MARK: - setup http headers
    private func httpHeaders() -> (HTTPHeaders) {
        return [
            "version"       :   Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "",
            "sdkVersion"    :   UIDevice.current.systemVersion,
            "platform"      :   "4",
            "Accept"        :   "application/json",
            "token"         :   OPTRuntime.sharedInstance().currentUser?.token ?? ""
        ]
    }
    
    // MARK: - 打印网络请求日志
    private func printRequestInfo(_ type: RequestType, _ request: YJBaseRequest) -> () {
        let method = (type == .get ? "GET" : "POST")
        JYPrint(NSString.init(format: "\n\n  type:%@\n  Url: %@%@\n  params: %@\n%@\n\n", method, YJBaseURLString , request.urlString ?? "", request.parameters, self.httpHeaders()))
    }

OC

- (void)requestWithType:(YJRequestType)type request:(YJBaseRequest *)request completion:(CompletionHandler)completion {
    if (request.urlString.length == 0) {
#ifdef DEBUG
        [SVProgressHUD showInfoWithStatus:@"兄弟,你没有设置请求路径。"];
#endif
        completion ? completion(NO, nil) : nil;
        return;
    }
    [self setupHttpSessionManagerHeader];
    
    YJWeakSelf;
    switch (type) {
        case YJRequestTypeGet: {
            [self.httpSessionManager GET:request.urlString parameters:request.parameters progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
                [weakSelf handleResponseResultsWithResponse:responseObject httpResponse:(NSHTTPURLResponse *)task.response error:nil completion:completion];
            } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                [weakSelf handleResponseResultsWithResponse:nil httpResponse:(NSHTTPURLResponse *)task.response error:error completion:completion];
            }];
        }
            break;
        case YJRequestTypePost: {
            [self.httpSessionManager POST:request.urlString parameters:request.parameters progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
                [weakSelf handleResponseResultsWithResponse:responseObject httpResponse:(NSHTTPURLResponse *)task.response error:nil completion:completion];
            } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                [weakSelf handleResponseResultsWithResponse:nil httpResponse:(NSHTTPURLResponse *)task.response error:error completion:completion];
            }];
        }
            break;
        default: {
            [SVProgressHUD showInfoWithStatus:@"未知请求。"];
            completion ? completion(NO, nil) : nil;
        }
            break;
    }
}

- (void)setupHttpSessionManagerHeader {
    // 设置请求头信息,比如token等
}

#pragma mark - 统一处理网络请求返回值

- (void)handleResponseResultsWithResponse:(id)responseObject
                             httpResponse:(NSHTTPURLResponse *)httpResponse
                                    error:(NSError *)error
                               completion:(CompletionHandler)completion {
    // 网络请求失败,上报Bugly
    NSInteger statusCode = httpResponse.statusCode;
    if (httpResponse && statusCode != 200) {
        // TODO: 上报异常
#ifdef DEBUG
        [SVProgressHUD showInfoWithStatus:[NSString stringWithFormat:@"statusCode = %ld, error = %@",statusCode, error.localizedDescription]];
#else
        [SVProgressHUD showInfoWithStatus:@"网络繁忙,请稍后重试!"];
#endif
        completion ? completion(NO, nil) : nil;
        return;
    }
    
    NSString *info = responseObject[@"resultMsg"];
    if (responseObject[@"resultCode"] == nil) {
        completion ? completion(NO, nil) : nil;
        [self showMessageWithError:error info:info];
        return;
    }
    
    NSInteger code = [responseObject[@"resultCode"] integerValue];
    switch (code) {
        case YJResponseCodeSuccess: {                                // 请求成功
            completion ? completion(YES, responseObject) : nil;
        }
            break;
        case YJResponseCodeTokenInvalid: {                           // token失效
            completion ? completion(NO, nil) : nil;
            // TODO: token失效处理
        }
            break;
        case YJResponseCodeRemoteLogined: {                          // 异地登录
            completion ? completion(NO, nil) : nil;
            // TODO: 异地登录处理
        }
            break;
        default: {                                                   // 其它错误
            completion ? completion(NO, nil) : nil;
            [self showMessageWithError:error info:info];
        }
            break;
    }
}

- (void)showMessageWithError:(NSError *)error info:(NSString *)info {
    if (error) {
        [SVProgressHUD showErrorWithStatus:error.localizedDescription];
    } else {
        if (info.length > 0) {
            [SVProgressHUD showErrorWithStatus:info];
        } else {
            [SVProgressHUD showInfoWithStatus:@"网络繁忙,请稍后重试!"];
        }
    }
}
  • 其它:
#pragma mark - 打印网络请求信息

- (void)logRequestInfoWithType:(YJRequestType)type  request:(YJBaseRequest *)request {
    NSString *methodStr = (type == YJRequestTypeGet ? @"GET" : @"POST");
    NSLog(@"\n\n  type:%@\n  Url: %@%@\n  params: %@\n%@\n\n", methodStr, YJBaseURLString, request.urlString, request.parameters, self.httpSessionManager.requestSerializer.HTTPRequestHeaders);
}

#pragma mark - setter and getter

- (AFHTTPSessionManager *)httpSessionManager {
    if (!_httpSessionManager) {
        _httpSessionManager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:YJBaseURLString]];
        //----- http header
        [_httpSessionManager.requestSerializer setValue:@"4" forHTTPHeaderField:@"platform"];
        [_httpSessionManager.requestSerializer setValue:[UIDevice currentDevice].systemVersion forHTTPHeaderField:@"sdkVersion"];
        [_httpSessionManager.requestSerializer setValue:YJAppVersion forHTTPHeaderField:@"version"];
        //----- https 适配 SecurityPolicy
//        _httpSessionManager.securityPolicy = [YJRequestManager customSecurityPolicy];
        _httpSessionManager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", @"text/html", nil];
    }
    return _httpSessionManager;
}

+ (AFSecurityPolicy*)customSecurityPolicy {
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"ymsj" ofType:@"cer"];//证书的路径
    NSData *certData = [NSData dataWithContentsOfFile:cerPath];
    
    // AFSSLPinningModeCertificate 使用证书验证模式
    AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
    
    // allowInvalidCertificates 是否允许无效证书(也就是自建的证书),默认为NO
    // 如果是需要验证自建证书,需要设置为YES
    securityPolicy.allowInvalidCertificates = YES;
    
    //validatesDomainName 是否需要验证域名,默认为YES;
    //假如证书的域名与你请求的域名不一致,需把该项设置为NO;如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议打开。
    //置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
    //如置为NO,建议自己添加对应域名的校验逻辑。
    securityPolicy.validatesDomainName = NO;
    securityPolicy.pinnedCertificates = [NSSet setWithObjects:certData, nil];
    return securityPolicy;
}

结尾

以上是个人对网络请求的简单理解,如有疑问或建议,欢迎积极留言探讨。>v<

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339