Swift:网络请求库——Alamofire

9,457 阅读6分钟

这是我参与更文挑战的第12天,活动详情查看: 更文挑战

Swift的网络请求库的技术选择

image.png

在Swift中,如果你要进行网络请求,基本上都会选择使用Alamofire,作为从OC时代就鼎鼎有名的AFNetworking的Swift版本,它不仅继承了AFN的优点,更是通过Swift语言的特性,将其更加升华。

你很难想象,在Swift中,如果不使用Alamofire做网络请求,还有什么其他选择?

Moya?说到底,它也是基于Alamofire的封装。

或者,在网络请求不太复杂的情况下,使用URLSession?

虽然还有其他的网络请求库,但是体量和Alamofire相比就是小巫见大巫了。

Alamofire5

随着版本的迭代,目前Alamofire跟随着Swift的脚步,已经来到了5.0时代,最新的稳定版本5.4.3。

我停留在Alamofire比较多的时间都是在4.0时代,4.8.2版本被我在很多项目中集成。

就我阅读的Alamofire5的源代码看,5.0是一个相当大的迭代,重新梳理了不少类,重新写了不少类,感觉就是整整大了一圈的感觉。

有兴趣的朋友可以看看Alamofire5的更新文档

Alamofire的开发文档亦是非常详细与完备,要学好与用好Alamofire,尝试通读文档,看代码是一个不错的选择喔——官方文档

觉得看英文很苦手的小伙伴,我也奉上掘金大佬里面对于Alamofire源码解读并添加了中文注释的资源——Alamofire源码学习目录合集

讲一讲使用Alamofire5的2个特性和1个注意事项

考虑写Alamofire的文章一大推,源码剖析也非常多,讲一些我觉得实用点的吧。

特性1:Decodable Responses

早在Swift4发布的时候,官方就提供了Codabl协议用于JSON转模型,Codable非常的简单易用,通过专用工具生成模版Model的话,简直是溜得一批。

不过遗憾的是,Alamofire4的时候,在请求回调过程中并不支持Codable。所以导致我写的时候,不得不这样来一把:

/// 定义个遵守Codable的模型
struct Item: Codable {
    var topicOrder: Int?
    var id: Int?
    var topicDesc: String?
    var topicTittle: String?
    var upTime: String?
    var topicImageUrl: String?
    var topicStatus: Int?
}

Alamofire.request("你的请求A网址", method: .get).response { response in
    guard let data = response.data else {
        return
    }
    
    /// Data通过JSONDecoder解析器转为模型
    guard let model = try? JSONDecoder().decode(Item.self, from: data) else {
        return
    }

    print(model)
}

没错,因为4.0时代没有将<T: Codabel>这种泛型引用到response方法中,导致你在每次写接口获取数据的时候都不得不这样来一把。虽然可以封装方法,不过整体还是不够友好。

所以当时我不得不撸了一个Alamofire的分类,甚至写了一个podspecs上传到私有库,专门干这个事:

import Foundation
import Alamofire

// MARK: - Codable

extension Request {
    /// 返回遵守Codable协议的Result类型
    ///
    /// - Parameters:
    ///   - response: 服务器返回的响应
    ///   - data: 服务器返回的数据
    ///   - error: AFError
    ///   - keyPath: 模型的keyPath 可解析深层的JSON数据
    /// - Returns: Result<T>
    public static func serializeResponseCodable<T: Codable>(response: HTTPURLResponse?, data: Data?, error: Error?, keyPath: String?) -> Result<T> {
        if let error = error { return .failure(error) }
        
        if let response = response, emptyDataStatusCodes.contains(response.statusCode) {
            do {
                let value = try JSONDecoder().decode(T.self, from: Data())
                return .success(value)
            } catch {
                return .failure(AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error)))
            }
        }
        
        guard let validData = data else {
            return .failure(AFError.responseSerializationFailed(reason: .inputDataNil))
        }
        
        if let keyPath = keyPath, !keyPath.isEmpty {
            var keyPaths = keyPath.components(separatedBy: "/")
            return keyPathForCodable(keyPaths: &keyPaths, data: validData)

        }else {
            do {
                let value = try JSONDecoder().decode(T.self, from: validData)
                return .success(value)
            } catch {
                return .failure(AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error)))
            }
        }
    }
    
    /// 通过键值路径寻找深层的JSON对应的模型
    ///
    /// - Parameters:
    ///   - keyPaths: 路径数组
    ///   - data: 数据
    /// - Returns: Result<T>
    private static func keyPathForCodable<T: Codable>(keyPaths: inout [String], data: Data)  -> Result<T> {
        if let firstKeyPath = keyPaths.first, keyPaths.count > 1 {
            keyPaths.remove(at: 0)
            if let JSONObject = try? JSONSerialization.jsonObject(with: data, options: .allowFragments),
                let keyPathJSONObject = (JSONObject as AnyObject?)?.value(forKeyPath: firstKeyPath),
                let keyPathData = try? JSONSerialization.data(withJSONObject: keyPathJSONObject) {
                return keyPathForCodable(keyPaths: &keyPaths, data: keyPathData)
            }
        }else if let lastKeyPath = keyPaths.last, keyPaths.count == 1  {
            keyPaths.remove(at: 0)
            if let JSONObject = try? JSONSerialization.jsonObject(with: data, options: .allowFragments),
                let keyPathJSONObject = (JSONObject as AnyObject?)?.value(forKeyPath: lastKeyPath),
                let keyPathData = try? JSONSerialization.data(withJSONObject: keyPathJSONObject) {
                do {
                    let value = try JSONDecoder().decode(T.self, from: keyPathData)
                    return .success(value)
                } catch {
                    return .failure(AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error)))
                }
            }
        }
        
        return .failure(AFError.responseSerializationFailed(reason: .inputDataNil))
    }
}

extension DataRequest {
    /// 创建一个遵守Codable协议的response serializer
    ///
    /// - Parameter keyPath: 键值路径
    /// - Returns: 遵守Codable协议的response serializer
    public static func codableResponseSerializer<T: Codable>(keyPath: String?) -> DataResponseSerializer<T> {
        return DataResponseSerializer { _, response, data, error in
            return Request.serializeResponseCodable(response: response, data: data, error: error, keyPath: keyPath)
        }
    }
    
    /// 添加一个请求完成的handle
    ///
    /// - Parameters:
    ///   - queue: 回调线程
    ///   - keyPath: 键值路径
    ///   - completionHandler: handle
    /// - Returns: DataRequest
    @discardableResult
    public func responseCodable<T: Codable>(
        queue: DispatchQueue? = nil,
        keyPath: String? = nil,
        completionHandler: @escaping (DataResponse<T>) -> Void)
        -> Self
    {
        return response(
            queue: queue,
            responseSerializer: DataRequest.codableResponseSerializer(keyPath: keyPath),
            completionHandler: completionHandler
        )
    }
}

通过这个分类后,我就可以开开心心的返回模型了:

Alamofire.request("你的Api", method: .get).responseCodable { (response: DataResponse<Item>) in
    guard let value = response.value else { return }
    print(value)
}

没什么太特别的地方,只是为了更加简洁。

而这一切在Alamofire5中有了转机,因为Alamofire官方已经在其源码里面添加了这个功能,并且可以使用Result回调数据,所以也不用什么分类就可以开开心心的JSON转模型了:

AF.request(baseURL,
           method: .post)
           .responseDecodable { (response: AFDataResponse<Item>) in
               switch response.result {
               case .success(let model):
                   break
               case .failure(let error):
                   break
               }
           }

是不是简洁很多呢?

特性2:Encodable Parameters

在Alamofire5之前,网络请求的Parameters我们用的就是字典。

在Alamofire中也是这么定义的:

/// Parameters其实就是一个字典的别名
public typealias Parameters = [String: Any]

/// 请求参数也是字典
public func request(
    _ url: URLConvertible,
    method: HTTPMethod = .get,
    parameters: Parameters? = nil,
    encoding: ParameterEncoding = URLEncoding.default,
    headers: HTTPHeaders? = nil)
    -> DataRequest

不过在Alamofire5后,传递一个基于Codable协议的模型作为请求参数也是可以的了!

我个人认为,这个功能早就该实现了!我自己在Alamofire没有这个功能前,对于那些传参特别的多的字典也会用模型去先接着,然后在转成字典进行请求。

请求参数传递的是模型,在请求内部统一将模型转为字典再进行请求,减少了因手写字典导致的硬编码错误!平常又重要。

Api:

open func request<Parameters: Encodable>(_ convertible: URLConvertible,
                                             method: HTTPMethod = .get,
                                             parameters: Parameters? = nil,
                                             encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default,
                                             headers: HTTPHeaders? = nil,
                                             interceptor: RequestInterceptor? = nil,
                                             requestModifier: RequestModifier? = nil) -> DataRequest

例子:

/// 定义一个传参模型
struct VinEntity: Codable {
    var vin: String
}

/// 创建一个传参实例
let vin = VinEntity(vin: "season")

/// Alamofire中请求中传递这个实例即可
AF.request(baseURL,
           method: .post,
           parameters: vin, encoder: JSONParameterEncoder.default)
    .responseDecodable { (response: AFDataResponse<Token>) in
        switch response.result {
        case .success(let model):
            break
        case .failure(let error):
            break
        }
    }

注意事项:encoding: ParameterEncoding与encoder: ParameterEncoder

encoding参数是针对入参是字典的请求参数编码方式,默认方式是URLEncoding.default

encoder参数是针对入参是遵守Codable协议的请求参数编码方式,默认方式是URLEncodedFormParameterEncoder.default

如果你做的网络请求其他入参都没有问题,而就是获取不到后台发给你的正确数据,可以考虑改改这个参数。

encoding: JSONEncoding.default

encoder: JSONParameterEncoder.default

明日继续

Alamofire之后就是基于它的封装层Moya了,端午节更文不易,大家加油!