Alamofire 4.0 迁移指南

原文: Alamofire 4.0 Migration Guide
作者: cnoon
译者: kemchenj

译者注:

最近打算把公司项目迁移到 Swift 3.0, 顺手把 Alamofire 4.0 的迁移指南翻译了, 之前虽然读过一部分源码, 但还是看到了很多新东西, 新的 Adapter 和 Retrier 我都打算用到项目里, 希望大家看完也能够有收获.

正文:

Alamofire 4.0 是 Alamofire 最新的一个大版本更新, 一个基于 Swift 的 iOS, tvOS, macOS, watchOS 的 HTTP 网络库. 作为一个大版本更新, 就像语义上那样, 4.0 的 API 引入了一些破坏性修改.

这篇导引旨在帮助大家从 Alamofire 3.x 平滑过渡到最新版本, 同时也解释一下新的设计和结构, 以及功能上的更新.

要求

  • iOS 8.0+, macOS 10.10.0+, tvOS 9.0+ 以及 watchOS 2.0+
  • Xcode 8.1+
  • Swift 3.0+

那些想要在 iOS 8 或者 macOS 10.9 使用 Alamofire 的, 请使用 3.x 版本的最新 release(同时支持 Swift 2.2以及2.3)

升级的好处

  • 完美适配 Swift 3: 跟进了新的 API 设计规范.
  • 新的错误处理系统: 根据提案 SE-0112 里的新模式, 新增了 AFError 类型.
  • 新的 RequestAdapter 协议: 可以在初始化 Request 的时候进行快速便捷的适配, 例如在请求头里加入 Authorization
  • 新的 RequestRetrier 协议: 可以检测并且重试失败的 Request, 甚至可以自己根据一系列需求去构建一套验证的解决方案( OAuth1, OAuth2, xAuth, Basic Auth 之类的).
  • 新的 Parameter Encoding 协议: 取代掉之前的 ParameterEncoding 枚举, 允许你更简单的拓展和自定义, 并且在错误时抛出异常, 而不是简单的返回一个元组.
  • 新的请求类型: 包括 DataRequest, DownloadRequest, UploadRequestStreamRequest, 实现了特定的进度, 验证和序列化的 API 以及各自的 Request 类型.
  • 新的进度 API: 包括 downloadProgressuploadProgress, 支持 progressInt64 类型, 并且会在指定的线程运行, 默认为主线程.
  • 更强大的数据验证: 在验证失败的时候, 包括 data 或者 temporaryURLdestinationURL 都可以使用内联的闭包去转化服务器返回的错误信息
  • 新的下载地址处理: 你可以获得完整的控制权, 而不是像之前那样只是提供一个 destinationURL, 还得创建临时文件夹, 删掉之前的文件.
  • 新的 Response 类型: 统一 response 的 API, 并且为所有下载任务提供 temporaryURLdownloadURL, 以及其它新平台上的任务属性.

API 破坏性的修改

Alamofire 4 跟进了 Swift 3 里所有的修改, 包括 API 设计规范. 因此, 几乎所有 Alamofire 的 API 都进行了一定程度的修改. 我们没办法把这些修改全部在文档里列出来, 所以我们会把最常用的那些 API 列出来, 然后告诉大家这些 API 进行了哪些修改, 而不是指望那些有时帮倒忙的编译错误提示.

命名空间的修改

一些常用的类移到了全局命名空间成为一级类, 让他们更容易使用.

  • Manager 改为 SessionManager
  • Request.TaskDelegate 改为 TaskDelegate
  • Request.DataTaskDelegate 改为 DataTaskDelegate
  • Request.DownloadTaskDelegate 改为 DownloadTaskDelegate
  • Request.UploadTaskDelegate 改为 UploadTaskDelegate

我们也重新调整了文件结构和组织模式, 帮助更好的跟进代码. 我们希望这可以让更多用户去了解内部结构和 Alamofire 的具体实现. 只是就是力量.

生成请求

生成请求是 Alamofire 里最主要的操作, 这里有 3.x 以及 4 的等效代码对比.

Data Request - Simple with URL string

// Alamofire 3
Alamofire.request(.GET, urlString).response { request, response, data, error in
    print(request)
    print(response)
    print(data)
    print(error)
}

// Alamofire 4
Alamofire.request(urlString).response { response in // 默认为 `.get` 方法
    debugPrint(response)
}

Data Request - Complex with URL string

// Alamofire 3
let parameters: [String: AnyObject] = ["foo": "bar"]

Alamofire.request(.GET, urlString, parameters: parameters, encoding: .JSON)
        .progress { bytesRead, totalBytesRead, totalBytesExpectedToRead in
                print("Bytes: \(bytesRead), Total Bytes: \(totalBytesRead), Total Bytes Expected: \(totalBytesExpectedToRead)")
        }
        .validate { request, response in
                // 自定义的校验闭包 (访问不到服务器返回的数据)
            return .success
        }
    .responseJSON { response in
                debugPrint(response)
        }

// Alamofire 4
let parameters: Parameters = ["foo": "bar"]

Alamofire.request(urlString, method: .get, parameters: parameters, encoding: JSONEncoding.default)
        .downloadProgress(queue: DispatchQueue.global(qos: .utility)) { progress in
                print("进度: \(progress.fractionCompleted)")
        }
        .validate { request, response, data in
                // 自定义的校验闭包, 现在加上了 `data` 参数(允许你提前转换数据以便在必要时挖掘到错误信息)
            return .success
        }
    .responseJSON { response in
                debugPrint(response)
        }

Download Request - Simple With URL string

// Alamofire 3
let destination = DownloadRequest.suggestedDownloadDestination()

Alamofire.download(.GET, urlString, destination: destination).response { request, response, data, error in
      // fileURL 在哪, 怎么获取?
    print(request)
    print(response)
    print(data)
    print(error)
}

// Alamofire 4
let destination = DownloadRequest.suggestedDownloadDestination()

Alamofire.download(urlString, to: destination).response { response in // 默认为 `.get` 方法
    print(response.request)
    print(response.response)
        print(response.temporaryURL)
        print(response.destinationURL)
    print(response.error)
}

Download Request - Simple With URLRequest

// Alamofire 3
let destination = DownloadRequest.suggestedDownloadDestination()

Alamofire.download(urlRequest, destination: destination).validate().responseData { response in
      // fileURL 在哪里, 太难获取了
        debugPrint(response)
}

// Alamofire 4
Alamofire.download(urlRequest, to: destination).validate().responseData { response in
        debugPrint(response)
        print(response.temporaryURL)
        print(response.destinationURL)
}

Download Request - Complex With URL String

// Alamofire 3
let fileURL: NSURL
let destination: Request.DownloadFileDestination = { _, _ in fileURL }
let parameters: [String: AnyObject] = ["foo": "bar"]

Alamofire.download(.GET, urlString, parameters: parameters, encoding: .JSON, to: destination)
        .progress { bytesRead, totalBytesRead, totalBytesExpectedToRead in
                print("Bytes: \(bytesRead), Total Bytes: \(totalBytesRead), Total Bytes Expected: \(totalBytesExpectedToRead)")
        }
        .validate { request, response in
                // 自定义的校验实现(获取不到临时下载位置和目标下载位置)
            return .success
        }
        .responseJSON { response in
                print(fileURL) // 只有在闭包捕获了的情况才能获取到, 不够理想
                debugPrint(response)
        }

// Alamofire 4
let fileURL: URL
let destination: DownloadRequest.DownloadFileDestination = { _, _ in
        return (fileURL, [.createIntermediateDirectories, .removePreviousFile])
}
let parameters: Parameters = ["foo": "bar"]

Alamofire.download(urlString, method: .get, parameters: parameters, encoding: JSONEncoding.default, to: destination)
        .downloadProgress(queue: DispatchQueue.global(qos: .utility)) { progress in
                print("进度: \(progress.fractionCompleted)")
        }
        .validate { request, response, temporaryURL, destinationURL in
                // 自定义的校验闭包, 现在包含了 fileURL (必要时可以获取到错误信息)
            return .success
        }
        .responseJSON { response in
                debugPrint(response)
                print(response.temporaryURL)
                print(response.destinationURL)
        }

Upload Request - Simple With URL string

// Alamofire 3
Alamofire.upload(.POST, urlString, data: data).response { request, response, data, error in
    print(request)
    print(response)
    print(data)
    print(error)
}

// Alamofire 4
Alamofire.upload(data, to: urlString).response { response in // 默认为 `.post` 方法
    debugPrint(response)
}

Upload Request - Simple With URLRequest

// Alamofire 3
Alamofire.upload(urlRequest, file: fileURL).validate().responseData { response in
        debugPrint(response)
}

// Alamofire 4
Alamofire.upload(fileURL, with: urlRequest).validate().responseData { response in
        debugPrint(response)
}

Upload Request - Complex With URL string

// Alamofire 3
Alamofire.upload(.PUT, urlString, file: fileURL)
        .progress { bytes, totalBytes, totalBytesExpected in
                // 这里的进度是上传还是下载的?
                print("Bytes: \(bytesRead), Total Bytes: \(totalBytesRead), Total Bytes Expected: \(totalBytesExpectedToRead)")
        }
        .validate { request, response in
                // 自定义的校验实现(获取不到服务端的数据)
            return .success
        }
        .responseJSON { response in
                debugPrint(response)
        }

// Alamofire 4
Alamofire.upload(fileURL, to: urlString, method: .put)
        .uploadProgress(queue: DispatchQueue.global(qos: .utility)) { progress in
                print("上传进度: \(progress.fractionCompleted)")
        }
        .downloadProgress { progress in // 默认在主队列调用
                print("下载进度: \(progress.fractionCompleted)")
        }
        .validate { request, response, data in
                // 自定义的校验闭包, 现在加上了 `data` 参数(允许你提前转换数据以便在必要时挖掘到错误信息)
            return .success
        }
    .responseJSON { response in
                debugPrint(response)
        }

就像你看到的, 有很多 API 破坏性的修改, 但常用的 API 还是沿用了原来的设计, 但现在能够通过一行代码去生成更多更复杂的请求, 保持秩序的同时更加简洁.

URLStringConvertible 协议

URLStringConvertible 协议有两个很小的改变.

URLConvertible

第一个没什么了不起的"大"改变就是 URLStringConvertible 已经被重命名为 URLConvertible. 在 3.x 里, URLStringConvertible 的定义是这样子的:

public protocol URLStringConvertible {
    var URLString: String { get }
}

现在在 Alamofire 4 里, URLConvertible 协议是这样定义的:

public protocol URLConvertible {
    func asURL() throws -> URL
}

就像你看到的, URLString 属性完全去掉了, 然后换成了可能会抛出异常的 asURL 方法. 为了解释这样做的原因, 我们先回顾一下.

Alamofire 一个最最常见的问题就是用户忘了对 URL 进行百分号编码, 导致 Alamofire 崩溃掉. 直到现在, 我们(Alamofire 团队)的态度都是 Alamofire 就是这么设计的, 而你的 URL 必须遵守 RFC 2396 协议. 但这对于社区来说并不那么好, 因为我们更希望 Alamofire 告诉我们的 URL 是不合法的而不是直接 crash 掉.

现在, 回到新的 URLConvertible 协议. Alamofire 之所以不能安全地处理不合规范的 URL 字符串, 事实上是因为 URLStringConvertible 安全性的缺失. Alamofire 不可能知道你是怎么造出一个不合法的 URL. 所以, 如果 URL 不能通通过 URLConvertible 被创建的话, 一个 AFError.invalidURL 的异常就会被抛出.

这个修改(以及其它很多修改都)可以让 Alamofire 安全地处理不合理的 URL, 并且会在回调里抛出异常.

URLRequest Conformance

URLRequest 不再遵守 URLStringConvertible, 现在是 URLConvertible. 但这也只是之前版本的一个延展而已, 并不那么重要. 不过这很可能会让 Alamofire 的 API 产生歧义. 因此, URLRequest 不再遵守 URLStringConvertible.

这意味着你不能在代码里像这样子做了:

let urlRequest = URLRequest(url: URL(string: "https://httpbin.org/get")!)
let urlString = urlRequest.urlString

在 Alamofire 4里, 你应该这么做:

let urlRequest = URLRequest(url: URL(string: "https://httpbin.org/get")!)
let urlString = urlRequest.url?.absoluteString

查看 PR-1505 以获取更多信息.

URLRequestConvertible

在 3.x 里, URLRequestConvertible 也会产生相同的歧义问题, 之前的 URLRequestConvertible 是这么定义的:

public protocol URLRequestConvertible {
    var URLRequest: URLRequest { get }
}

现在, 在 Alamofire 4 里, 变成了这样子:

public protocol URLRequestConvertible {
    func asURLRequest() throws -> URLRequest
}

就像看到的这样, URLRequest 属性被替换成了 asURLRequest 方法, 并且在生成 URLRequest 失败时会抛出异常.

这影响最大的可能是采用了 Router (路由)设计的你, 如果你用了 Router, 那你就不得不去改变, 但会变得更好! 你需要去实现 asURLRequest 方法, 在必要的时候会抛出异常. 你不再需要强制解包数据和参数, 或者在 do-catch 里构建一个 ParameterEncoding. 现在 Router 抛出的任何错误都可以由 Alamofire 帮你处理掉.

查看 PR-1505 以获取更多信息.

新功能

Request Adapter (请求适配器)

RequestAdapter 协议是 Alamofire 4 里的全新功能.

public protocol RequestAdapter {
    func adapt(_ urlRequest: URLRequest) throws -> URLRequest
}

它可以让每一个 SessionManager 生成的 Request 都在生成之前被解析并且按照规则适配. 一个使用适配器很典型的场景就是给请求添加一个 Authorization 的请求头.

class AccessTokenAdapter: RequestAdapter {
    private let accessToken: String

    init(accessToken: String) {
        self.accessToken = accessToken
    }

    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
        var urlRequest = urlRequest

        if urlRequest.urlString.hasPrefix("https://httpbin.org") {
            urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
        }

        return urlRequest
    }
}

let sessionManager = SessionManager()
sessionManager.adapter = AccessTokenAdapter(accessToken: "1234")

sessionManager.request("https://httpbin.org/get")

如果一个 Error 在适配过程中产生的话, 它会逐层抛出, 最后传递到 Request 的请求回调里.

查看 PR-1450 获取更多信息.

Request Retrier (请求重连)

RequestRetrier 是 Alamofire 4 的另一个全新协议.

public typealias RequestRetryCompletion = (_ shouldRetry: Bool, _ timeDelay: TimeInterval) -> Void

public protocol RequestRetrier {
    func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion)
}

它可以在 Request 遇到 Error的时候, 在指定的延迟之后重新发起.

class OAuth2Handler: RequestAdapter, RequestRetrier {
    public func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: RequestRetryCompletion) {
        if let response = request.task.response as? HTTPURLResponse, response.statusCode == 401 {
            completion(true, 1.0) // 1秒后重试
        } else {
            completion(false, 0.0) // 不重连
        }
    }
}

let sessionManager = SessionManager()
sessionManager.retrier = OAuth2Handler()

sessionManager.request(urlString).responseJSON { response in
    debugPrint(response)
}

重连器可以让你在检测到 Request 完成并且完成所有 Validation 检测之后再考虑是否重试. 当 RequestAdapterRequestRetrier 一起使用的时候, 你可以给 OAuth1, OAuth2, Basic Auth 创建一套持续更新的校验系统(credential refresh systems), 甚至是快速重试的策略. 可能性是无限的. 想要获取更多关于这个话题的信息和例子, 请查看 README.

译者注: 这里没太能理解作者的意思, 翻译得不好, 直接放原文:
When using both the RequestAdapter and RequestRetrier protocols together, you can create credential refresh systems for OAuth1, OAuth2, Basic Auth and even exponential backoff retry policies.

查看 PR-1391 以及 PR-1450 获取更多信息.

Task Metrics

在 iOS, tvOS 10 和 macOS 10.12 里, 苹果引入了新的 URLSessionTaskMetrics API, task metrics 包含了一些 request 和 response 的统计信息, API 跟 Alamofire 的 Timeline 很像, 但提供了许多 Alamofire 里获取不到的统计信息. 我们对这些新的 API 特别兴奋, 但把这些全部都暴露到每一个 Response 类型里意味着这并不容易使用.

Alamofire.request(urlString).response { response in
        debugPrint(response.metrics)
}

有一点很重要的是, 这些 API 只有在 iOS 和 tvOS 10+ 和 macOS 10.12+上才能使用. 所以它是依赖于运行设备的, 你可能需要做可行性检查.

Alamofire.request(urlString).response { response in
    if #available(iOS 10.0, *) {
                debugPrint(response.metrics)
    }
}

查看 PR-1492 获取更多信息.

Updated Features 更新的功能

Alamofire 4 加强了现有的功能并且加入了很多新功能. 这一章节主要是大概地过一遍功能的更新和使用方式. 如果想要获取更多相关信息, 请点进链接查看相关的 pull request.

Errors 异常

Alamofire 4 加入了全新的异常系统, 采用了提案 SE-0112 里提出的新模式. 新的异常系统主要围绕 AFError, 一个继承了 Error 的枚举类型, 包含四个主要的 case.

  • .invalidURL(url: URLConvertible) - 创建 URL 失败的时候返回一个 URLConvertible 类型的值
  • .parameterEncodingFailed(reason: ParameterEncodingFailureReason) - 当其中一个参数编码出错的时候就会抛出错误并返回
  • .multipartEncodingFailed(reason: MultipartEncodingFailureReason) - multipart 编码出错就会抛出错误并返回
  • .responseValidationFailed(reason: ResponseValidationFailureReason) - 当调用 validate() 抛出错误时捕获然后抛出到外部.
  • .responseSerializationFailed(reason: ResponseSerializationFailureReason) - 返回的数据序列化出错时会抛出异常并返回.

每一个 case 都包含了特定的异常理由, 并且异常理由又是另一个带有具体错误信息的枚举类型. 这会让 Alamofire 更容易识别出错误的来源和原因.

Alamofire.request(urlString).responseJSON { response in
    guard case let .failure(error) = response.result else { return }

    if let error = error as? AFError {
        switch error {
        case .invalidURL(let url):
            print("无效 URL: \(url) - \(error.localizedDescription)")
        case .parameterEncodingFailed(let reason):
            print("参数编码失败: \(error.localizedDescription)")
            print("失败理由: \(reason)")
        case .multipartEncodingFailed(let reason):
            print("Multipart encoding 失败: \(error.localizedDescription)")
            print("失败理由: \(reason)")
        case .responseValidationFailed(let reason):
            print("Response 校验失败: \(error.localizedDescription)")
            print("失败理由: \(reason)")

            switch reason {
            case .dataFileNil, .dataFileReadFailed:
                print("无法读取下载文件")
            case .missingContentType(let acceptableContentTypes):
                print("文件类型不明: \(acceptableContentTypes)")
            case .unacceptableContentType(let acceptableContentTypes, let responseContentType):
                print("文件类型: \(responseContentType) 无法读取: \(acceptableContentTypes)")
            case .unacceptableStatusCode(let code):
                print("请求返回状态码出错: \(code)")
            }
        case .responseSerializationFailed(let reason):
            print("请求返回内容序列化失败: \(error.localizedDescription)")
            print("失败理由: \(reason)")
        }

        print("错误: \(error.underlyingError)")
    } else if let error = error as? URLError {
        print("URL 错误: \(error)")
    } else {
        print("未知错误: \(error)")
    }
}

新的设计给你的处理方式更多的自由, 可以在你需要的时候深入到最具体的 error. 这也会让原本要四处应对 NSError 的开发者更加轻松地完成工作. 在 Alamofire 里通过使用自定义的 Error 类型, 我们可以看到 ResultResponse 的泛型参数缩减到了只有一个, 简化了返回数据序列化的逻辑.

查看 PR-1419 获取更多信息.

Parameter Encoding Protocol 参数编码的协议

ParameterEncoding 枚举类型在过去两年很好地解决了问题. 但我们在 Alamofire 4 里想要定位的时候却感觉到了一些局限.

  • .url 总让人有点迷惑, 因为它是一个 HTTP 协议定义的地址
  • .urlEncodedInURL.url 总是会混淆起来, 让人分不清它们行为的区别
  • .JSON.PropertyList 编码不能自定义编码格式或者写入的方式
  • .Custom 编码对于用户来说太难掌握

因为这些原因, 我们决定在 Alamofire 4 把这个枚举去掉! 现在, ParameterEncoding 变成了一个协议, 加入了 Parameters 的类型别名去创建你的参数字典, 并且通过遵守这个协议建立了三个编码结构体 URLEncoding, JSONEncodingPropertyList.

public typealias Parameters = [String: Any]

public protocol ParameterEncoding {
    func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
}

URL Encoding (参数编码)

新的 URLEncoding 结构体包含了一个 Destination 枚举, 支持三种类型的目标编码

  • .methodDependent - 对于 GET, HEADDELETE 方法使用 query 字符串, 而别的 HTTP 方法则会编码为 HTTP body.
  • .queryString - 设置或者往现有的 queryString 里增加内容
  • .httpBody - 设置请求的 HTTP body 内容

这些目标编码格式会让你更容易控制 URLRequest 的参数编码方式. 创建请求依旧使用和之前一样的方式, 不管编码的形式怎样, 都会保持与之前一样的默认行为.

let parameters: Parameters = ["foo": "bar"]

Alamofire.request(urlString, parameters: parameters) // Encoding => URLEncoding(destination: .methodDependent)
Alamofire.request(urlString, parameters: parameters, encoding: URLEncoding(destination: .queryString))
Alamofire.request(urlString, parameters: parameters, encoding: URLEncoding(destination: .httpBody))

// Static convenience properties (we'd like to encourage everyone to use this more concise form)
// 便利的静态属性 (我们想鼓励大家使用这种更简洁的形式)
Alamofire.request(urlString, parameters: parameters, encoding: URLEncoding.default)
Alamofire.request(urlString, parameters: parameters, encoding: URLEncoding.queryString)
Alamofire.request(urlString, parameters: parameters, encoding: URLEncoding.httpBody)

JSON Encoding (JSON 编码)

新的 JSONEncoding 结构体开放了让你自定义 JSON 写入形式的接口.

let parameters: Parameters = ["foo": "bar"]

Alamofire.request(urlString, parameters: parameters, encoding: JSONEncoding(options: []))
Alamofire.request(urlString, parameters: parameters, encoding: JSONEncoding(options: .prettyPrinted))

// Static convenience properties (we'd like to encourage everyone to use this more concise form)
// 便利的静态属性 (我们想鼓励大家使用这种更简洁的形式)
Alamofire.request(urlString, parameters: parameters, encoding: JSONEncoding.default)
Alamofire.request(urlString, parameters: parameters, encoding: JSONEncoding.prettyPrinted)

Property List Encoding (属性列表编码)

新的 PropertyListEncoding 结构体允许自定义 plist 的格式和写入选项

let parameters: Parameters = ["foo": "bar"]

Alamofire.request(urlString, parameters: parameters, encoding: PropertyListEncoding(format: .xml, options: 0))
Alamofire.request(urlString, parameters: parameters, encoding: PropertyListEncoding(format: .binary, options: 0))

// Static convenience properties (we'd like to encourage everyone to use this more concise form)
// 便利的静态属性 (我们想鼓励大家使用这种更简洁的形式)
Alamofire.request(urlString, parameters: parameters, encoding: PropertyListEncoding.xml)
Alamofire.request(urlString, parameters: parameters, encoding: PropertyListEncoding.binary)

Custom Encoding 自定义编码

建立一个自定义的 ParameterEncoding 只要遵守这个协议建立类型即可. 想要获取更多相关例子, 请查看下面的 README

查看 PR-1465 获取更多信息

Request Subclasses (Request 的子类)

在 Alamofire 4, request, download, uploadstream 的 API 不会再返回 Request, 他们会返回特定的 Request 子类. 有下面几个引导我们做出这个改变的现实原因和社区的疑问:

  • Progress: progress 方法的行为会在 upload 请求里会很容易让人迷惑.
    • progress 在一个 upload 请求里返回的是什么? 上传的进度? 还是返回内容的下载进度?
    • 如果都返回, 那我们怎么区分他们, 在什么时候能知道是到底返回的是哪一个?
  • Response Serializers: 返回内容的序列化是为了 data 和 upload 请求设计的, donwload 和 stream 请求并不需要序列化.
    • 你要怎么才能在下载完成时获取到文件的地址?
    • responseData, responseStringresponseJSON 对于一个 donwload 请求来说意味着什么? stream 请求呢?

Alamofire 4 现在有四个 Request 的子类, 并且每个字类都有一些特有的 API. 这样就可以让每一个子类能够通过建立 extension 来定制特定类型的请求.

open class Request {
    // 包含了共有的属性, 验证, 和状态方法
    // 遵守 CustomStringConvertible 和 CustomDebugStringConvertible
}

open class DataRequest: Request {
    // 包含了数据流(不要跟 StreamRequest 混淆)和下载进度的方法
}

open class DownloadRequest: Request {
    // 包含了下载位置和选项, 已下载的数据以及进度方法
}

open class UploadRequest: DataRequest {
        // 继承了所有 DataRequest 的方法, 并且包含了上传进度的方法
}

open class StreamRequest: Request {
        // 只继承了 Request, 目前暂时没有任何自定义的 API
}

通过这样的切分, Alamofire 现在可以为每一个类型的请求自定义相关的 API. 这会覆盖到所有可能的需求, 但让我们花点时间来仔细了解一下这会如何改变进度汇报和下载地址.

查看 PR-1455 获取更多信息

Download and Upload Progress (下载和上传你进度)

Data, download 和 upload 请求的进度汇报系统完全重新设计了一遍. 每一个请求类型都包含有一个闭包, 每当进度更新的时候, 就会调用闭包并且传入 Progress 类型的参数. 这个闭包会在指定的队列被调用, 默认为主队列.

Data Request 进度

Alamofire.request(urlString)
    .downloadProgress { progress in
        // 默认在主队列调用
        print("下载进度: \(progress.fractionCompleted)")
    }
    .responseJSON { response in
        debugPrint(response)
    }

Download Request 进度

Alamofire.download(urlString, to: destination)
    .downloadProgress(queue: DispatchQueue.global(qos: .utility)) { progress in
        // 在 .utility 队列里调用
        print("下载进度: \(progress.fractionCompleted)")
    }
    .responseJSON { response in
        debugPrint(response)
    }

Upload Request 进度

Alamofire.upload(data, to: urlString, withMethod: .post)
    .uploadProgress { progress in
        // 默认在主队列调用
        print("上传进度: \(progress.fractionCompleted)")
    }
    .downloadProgress { progress in
        // 默认在主队列调用
        print("下载进度: \(progress.fractionCompleted)")
    }
    .responseData { response in
        debugPrint(response)
    }

现在很容易就可以区分开 upload request 里的上传和下载进度.

查看 PR-1455 获取更多信息.

Download File Destinations 文件下载地址

在 Alamofire 3.x, 顺利完成的 download requests 总是会在 destination 回调里把临时文件移动到最终目标文件夹里. 这很方便, 但也同时带来了几个限制:

  • Forced - API 强制你去提供一个 destination 闭包来移动文件, 即使你验证过后不想移动文件了.
  • Limiting - 没有任何方式可以去调整文件系统移动文件的优先级别.
    • 如果你需要在移动到目标文件夹之前删掉之前存在的文件呢?
    • 如果你需要在移动临时文件之前创建目录呢?

这些限制都会在 Alamofire 4 里都不复存在. 首先是 optional 的 destination 闭包. 现在, destination 默认为 nil, 意味着文件系统不会移动文件, 并且会返回临时文件的 URL.

Alamofire.download(urlString).responseData { response in
    print("临时文件的 URL: \(response.temporaryURL)")
}

我们将会恢复 DownloadResponse 类型, 更多详细信息请查看 Reponse Serializers 章节.

Download Options 下载选项

另外一个主要的改变是 destination 闭包里面加上了下载选项, 让你可以进行更多文件系统操作. 为了达到目的, 我们建立了一个 DownloadOptions 类型并且添加到 DownloadFileDestination 闭包里.

public typealias DownloadFileDestination = (
    _ temporaryURL: URL,
    _ response: HTTPURLResponse)
    -> (destinationURL: URL, options: DownloadOptions)

现阶段支持的两个 DownloadOptions 是:

  • .createIntermediateDirectories - 如果有指定的下载地址的话, 会为下载地址创建相应的目录
  • .removePreviousFile - 如果有指定的下载地址的话, 会自动替代掉同名文件

这两个选项可以像下面这样用:

let destination: DownloadRequest.DownloadFileDestination = { _, _ in
    return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}

Alamofire.download(urlString, to: destination).response { response in
    debugPrint(response)
}

如果一个异常在文件系统操作时抛出的话, DownloadResponseerror 就会是 URLError 类型.

查看 PR-1462 获取更多信息.

Response Validation 数据验证

在 Alamofire 4 里有几个可以加强数据验证系统的地方. 包括了:

  • Validation 回调闭包里传入的 data
  • Request 子类可以自定义数据验证系统, 例如 download 请求里的 temporaryURLdestinationURL 暴露到了回调闭包里

通过继承 Request, 每一个 Request 的子类都可以自定义一套数据验证的闭包(typealias)和请求的 API.

Data Request 数据请求

DataRequest (UploadRequest 的父类)暴露出来的 Validation 目前是这样定义的:

extension DataRequest {
    public typealias Validation = (URLRequest?, HTTPURLResponse, Data?) -> ValidationResult
}

直接在闭包里把 Data? 暴露出来, 你就不需要再给 Request 增加一个 extension 去访问这个属性了. 现在你可以直接这样子做:

Alamofire.request(urlString)
    .validate { request, response, data in
        guard let data = data else { return .failure(customError) }

        // 1) 验证返回的数据保证接下来的操作不会出错
        // 2) 如果验证失败, 你可以把错误信息返回出去, 甚至加上自定义的 error

        return .success
    }
    .response { response in
        debugPrint(response)
    }

Download Request 下载请求

DownloadRequest 里的 Validation 闭包跟 DataRequest 里的很像, 但为了下载任务做了更多的定制.

extension DownloadRequest {
        public typealias Validation = (
            _ request: URLRequest?,
            _ response: HTTPURLResponse,
            _ temporaryURL: URL?,
            _ destinationURL: URL?)
            -> ValidationResult
}

temporaryURLdestinationURL 参数现在让你可以在闭包内直接获取到服务器返回的数据. 这可以让你校验下载好的文件, 在有需要的时候可以抛出一个自定义的错误.

Alamofire.download(urlString)
    .validate { request, response, temporaryURL, destinationURL in
        guard let fileURL = temporaryURL else { return .failure(customError) }

        do {
            let _ = try Data(contentsOf: fileURL)
            return .success
        } catch {
            return .failure(customError)
        }
    }
    .response { response in
        debugPrint(response)
    }

通过直接在闭包里暴露服务器返回的数据, 这里面的所有异常都可以在 Validation 闭包里捕获到, 并且可以自定义错误信息. 如果这里获取到的信息和 response 序列化回调里一样的话, response 可以用来处理错误信息而不是简单地把逻辑赋值过来. 具体的例子, 请查看下面的 README.

查看 PR-1461 获取更多信息.

Response Serializers 返回数据序列化

Alamofire 3.x 里的序列化系统有这么几个限制:

  • 序列化的 API 可以用在 download 和 stream 请求里, 但却会导致未知的行为发生
    • 怎么在下载成功时获取到文件 URL?
    • responseData, responseString 或者 responseJSON 会在 donwload 请求里产生怎样的行为? stream 请求呢?
  • response API 返回四个参数而不是封装到一个 Response 类型里.
    • 最大的问题是 API 任何改变都会导致前面行为的变化.
    • 在序列化和反序列化的 API 之间切换会让人迷惑, 同时导致难以 debug 的编译错误.

就像你看到的, Alamofire 3.x 的这一套序列化系统有这么多限制. 所以, 在 Alamofire 4里, Request 类型首先被切分到各个子类里, 这么做给自定义序列化方式, 和自定义 API 留下了空间. 在我们更深入了解序列化方式之前, 我们先了解一下新的 Response 类型

Default Data Response

DefaultDataResponse 代表了未被序列化的服务器返回数据. Alamofire 没有做任何处理过的, 只是纯粹地从 SessionDelegate 里获取信息并且包装在一个结构体里面返回.

public struct DefaultDataResponse {
    public let request: URLRequest?
    public let response: HTTPURLResponse?
    public let data: Data?
    public let error: Error?
    public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics }
}

下面是你会获得 DataRequest.response 的一种返回.

Alamofire.request(urlString).response { response in
    debugPrint(response)
}

Alamofire.upload(file, to: urlString).response { response in
    debugPrint(response)
}

Data Response

泛型 DataResponse 类型跟 Alamofire 3.x 里的 Response 一样, 但内部重构并且包含了新的 metrics 变量.

public struct DataResponse<Value> {
    public let request: URLRequest?
    public let response: HTTPURLResponse?
    public let data: Data?
    public let result: Result<Value>
    public let timeline: Timeline
        public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics }
}

使用 DataRequestUploadRequest, 你可以像之前(3.x)那样使用 response 序列化的 API

Alamofire.request(urlString).responseJSON { response in
    debugPrint(response)
    print(response.result.isSuccess)
}

Alamofire.upload(fileURL, to: urlString).responseData { response in
    debugPrint(response)
    print(response.result.isSuccess)
}

Default Download Response 默认下载请求的 Response 类型

因为 donwload 请求跟 data 和 upload 请求很不一样, 所以 Alamofire 4 包含了自定义的 donwload Response 类型. DefaultDownloadResponse 类型代表未序列化的返回数据, 包含了所有 SessionDelegate 信息的结构体.

public struct DefaultDownloadResponse {
    public let request: URLRequest?
    public let response: HTTPURLResponse?
    public let temporaryURL: URL?
    public let destinationURL: URL?
    public let resumeData: Data?
    public let error: Error?
        public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics }
}

DefaultDownloadResponse 类型在使用新的 DownloadRequest.response API 时就会被返回.

Alamofire.download(urlString).response { response in
    debugPrint(response)
    print(response.temporaryURL)
}

Download Response

新的泛型 DownloadResponseDataResponse 很像, 但包含了 download 请求特有的信息. DownloadResponse 类型在使用 DownloadRequest 时就会被返回. 这些新的 API 同样也适用于 DataRequest, 一样能够获取临时目录的 url 和目标目录的 url.

Alamofire.download(urlString, to: destination)
    .responseData { response in
        debugPrint(response)
    }
    .responseString { response in
        debugPrint(response)
    }
    .responseJSON { response in
        debugPrint(response)
    }
    .responsePropertyList { response in
        debugPrint(response)
    }

新的序列化 API 让文件下载和序列化更加容易完成.

Custom Response Serializers 自定义序列化

如果你已经创建了自定义的序列化, 你也许会想要拓展支持 data 和 download 请求, 就像我们在 Alamofire 序列化 API 里面做的一样.. 如果你决定这么做, 可以仔细看一下 Alamofire 怎么在几种 Request 类型里共享序列化方法, 然后把实现写到 Request 里就可以了. 这可以让我们 DRY up 逻辑并且避免重复的代码.(Don't repeat yourself)

查看 PR-1457 获取更多信息.

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

推荐阅读更多精彩内容