Alamofire 4.0 遷移指南


 

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 枚舉, 允許你更簡單的拓展和自定義, 並且在錯誤時拋出異常, 而不是簡單的返回一個元組.
  • 新的請求類型: 包括 DataRequestDownloadRequestUploadRequest 和 StreamRequest, 實現了特定的進度, 驗證和序列化的 API 以及各自的 Request 類型.
  • 新的進度 API: 包括 downloadProgress 和 uploadProgress, 支持 progress 和 Int64 類型, 並且會在指定的線程運行, 默認為主線程.
  • 更強大的數據驗證: 在驗證失敗的時候, 包括 data 或者 temporaryURL 和 destinationURL 都可以使用內聯的閉包去轉化服務器返回的錯誤信息
  • 新的下載地址處理: 你可以獲得完整的控制權, 而不是像之前那樣只是提供一個 destinationURL, 還得創建臨時文件夾, 刪掉之前的文件.
  • 新的 Response 類型: 統一 response 的 API, 並且為所有下載任務提供 temporaryURL 和 downloadURL, 以及其它新平台上的任務屬性.

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 檢測之后再考慮是否重試. 當 RequestAdapter和 RequestRetrier 一起使用的時候, 你可以給 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 類型, 我們可以看到 Result 和Response 的泛型參數縮減到了只有一個, 簡化了返回數據序列化的邏輯.

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

Parameter Encoding Protocol 參數編碼的協議

ParameterEncoding 枚舉類型在過去兩年很好地解決了問題. 但我們在 Alamofire 4 里想要定位的時候卻感覺到了一些局限.

  • .url 總讓人有點迷惑, 因為它是一個 HTTP 協議定義的地址
  • .urlEncodedInURL 跟 .url 總是會混淆起來, 讓人分不清它們行為的區別
  • .JSON 和 .PropertyList 編碼不能自定義編碼格式或者寫入的方式
  • .Custom 編碼對於用戶來說太難掌握

因為這些原因, 我們決定在 Alamofire 4 把這個枚舉去掉! 現在, ParameterEncoding 變成了一個協議, 加入了Parameters 的類型別名去創建你的參數字典, 並且通過遵守這個協議建立了三個編碼結構體 URLEncoding,JSONEncoding 和 PropertyList.

public typealias Parameters = [String: Any]
 
public protocol ParameterEncoding {
func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
}

URL Encoding (參數編碼)

新的 URLEncoding 結構體包含了一個 Destination 枚舉, 支持三種類型的目標編碼

  • .methodDependent - 對於 GETHEAD 和 DELETE 方法使用 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, requestdownloadupload 和 stream 的 API 不會再返回 Request, 他們會返回特定的Request 子類. 有下面幾個引導我們做出這個改變的現實原因和社區的疑問:

  • Progress: progress 方法的行為會在 upload 請求里會很容易讓人迷惑.
    • progress 在一個 upload 請求里返回的是什么? 上傳的進度? 還是返回內容的下載進度?
    • 如果都返回, 那我們怎么區分他們, 在什么時候能知道是到底返回的是哪一個?
  • Response Serializers: 返回內容的序列化是為了 data 和 upload 請求設計的, donwload 和 stream 請求並不需要序列化.
    • 你要怎么才能在下載完成時獲取到文件的地址?
    • responseDataresponseString 和 responseJSON 對於一個 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)
}

如果一個異常在文件系統操作時拋出的話, DownloadResponse 的 error 就會是 URLError 類型.

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

Response Validation 數據驗證

在 Alamofire 4 里有幾個可以加強數據驗證系統的地方. 包括了:

  • Validation 回調閉包里傳入的 data
  • Request 子類可以自定義數據驗證系統, 例如 download 請求里的 temporaryURL 和 destinationURL 暴露到了回調閉包里

通過繼承 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
}

temporaryURL 和 destinationURL 參數現在讓你可以在閉包內直接獲取到服務器返回的數據. 這可以讓你校驗下載好的文件, 在有需要的時候可以拋出一個自定義的錯誤.

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?
    • responseDataresponseString 或者 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 }
}

使用 DataRequest 和 UploadRequest, 你可以像之前(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

新的泛型 DownloadResponse 跟 DataResponse 很像, 但包含了 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)

 

原文鏈接:https://kemchenj.github.io/2017/01/06/2016-11-30/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM