这是我参与更文挑战的第12天,活动详情查看: 更文挑战
Swift的网络请求库的技术选择
在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了,端午节更文不易,大家加油!