Alamofire 4.0 遷移指南

原文: Alamofire 4.0 Migration Guide
做者: cnoon
譯者: kemchenjhtml

譯者注:最近打算把公司項目遷移到 Swift 3.0, 順手把 Alamofire 4.0 的遷移指南翻譯了, 以前雖然讀過一部分源碼, 但仍是看到了不少新東西, 新的 Adapter 和 Retrier 我都打算用到項目裏, 但願你們看完也可以有收穫.git


Alamofire 4.0 是 Alamofire 最新的一個大版本更新, 一個基於 Swift 的 iOS, tvOS, macOS, watchOS 的 HTTP 網絡庫. 做爲一個大版本更新, 就像語義上那樣, 4.0 的 API 引入了一些破壞性修改.github

這篇導引旨在幫助你們從 Alamofire 3.x 平滑過渡到最新版本, 同時也解釋一下新的設計和結構, 以及功能上的更新.swift

要求

  • iOS 8.0+, macOS 10.10.0+, tvOS 9.0+ 以及 watchOS 2.0+api

  • Xcode 8.1+安全

  • Swift 3.0+服務器

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

升級的好處

  • 完美適配 Swift 3: 跟進了新的 API 設計規範.session

  • 新的錯誤處理系統: 根據提案 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 獲取更多信息.

相關文章
相關標籤/搜索