Swift中的HTTP(十八) 總結

語言: CN / TW / HK

HTTP簡介

HTTP基礎結構

HTTP請求體

HTTP 載入請求

HTTP 模擬測試

HTTP 鏈式載入器

HTTP 動態修改請求

HTTP 請求選項

HTTP 重置

HTTP 取消

HTTP 限流

HTTP 重試

HTTP 基礎鑑權

HTTP 自動鑑權設定

HTTP 自動鑑權

HTTP 複合載入器

HTTP 頭腦風暴

HTTP 總結

在這個系列的過程中,我們從一個簡單的想法開始,並將它帶到了一些非常迷人的地方。 我們開始的想法是可以將網路層抽象為“我傳送此請求,最終我得到響應”的想法。

在閱讀 Rob Napier 關於協議協議的部落格文章後,我開始研究這種方法。 在其中,他指出我們似乎誤解了 Dave Abrahams Crusty 在 WWDC 2015 上提出的開創性的“面向協議程式設計”的想法。在談到網路時,我們尤其忽略了這一點,Rob 的後續帖子進一步探討了這個想法 .

我希望您在本系列博文中已經意識到的一件事是,在本系列中我從未談論過 Codable。 本系列中沒有任何內容是通用的(除了使指定請求主體變得容易的小例外)。 沒有提到反序列化或 JSON 或解碼響應或任何東西。 這是非常刻意的。

HTTP 的要點很簡單:您傳送一個 HTTP 請求(我們看到它具有非常明確的結構),您會返回一個 HTTP 響應(它具有類似的明確定義的結構)。 沒有機會介紹泛型,因為我們不是在處理通用演算法。

所以這引出了一個問題:泛型從何而來? 我如何在這個框架中使用我很棒的 Codable 型別? 答案是:下一層抽象。

Hello, Codable!

我們的 HTTP 堆疊處理具體的輸入型別 (HTTPRequest) 和具體的輸出型別 (HTTPResponse)。 沒有地方放通用的東西。 我們在某些時候需要泛型,因為我們想使用我們漂亮的 Codable 結構,但它們不屬於 HTTP 通訊層。

因此,我們將 HTTPLoader 鏈包裹在一個可以處理泛型的新層中。 我稱之為“連線”層,它看起來像這樣: ``` public class Connection {

private let loader: HTTPLoader

public init() {
    self.loader = ...
}

public func request(_ request: ..., completion: ...) {
    // TODO: create an HTTPRequest
    // TODO: interpret the HTTPResponse
}

} ```

為了以通用方式解釋響應,這就是我們需要泛型的地方,因為這是我們需要使其適用於許多不同型別的演算法。 因此,我們將定義一個通常包裝 HTTPRequest 並可以解釋 HTTPResponse 的型別: ``` public struct Request { public let underlyingRequest: HTTPRequest public let decode: (HTTPResponse) throws -> Response

public init(underlyingRequest: HTTPRequest, decode: @escaping (HTTPResponse) throws -> Response) {
    self.underlyingRequest = underlyingRequest
    self.decode = decode
}

} ```

當我們知道 Response 是 Decodable 時,我們還可以提供一些方便的方法:

``` extension Request where Response: Decodable {

// request a value that's decoded using a JSON decoder
public init(underlyingRequest: HTTPRequest) {
    self.init(underlyingRequest: underlyingRequest, decoder: JSONDecoder())
}

// request a value that's decoded using the specified decoder
// requires: import Combine
public init<D: TopLevelDecoder>(underlyingRequest: HTTPRequest, decoder: D) where D.Input == Data {
    self.init(underlyingRequest: underlyingRequest,
              decode: { try decoder.decode(Response.self, from: $0.body) })
}

} ```

有了這個,我們就有了一種方法來封裝“傳送這個 HTTPRequest 應該產生一個我可以使用這個閉包解碼的值”的想法。 我們現在可以實現我們之前刪除的請求方法:

``` public class Connection { ...

public func request<ResponseType>(_ request: Request<ResponseType>, completion: @escaping (Result<ResponseType, Error>) -> Void) {
    let task = HTTPTask(request: request.underlyingRequest, completion: { result in
        switch result {
            case .success(let response):

                do {
                    let response = try request.decode(httpResponse: response)
                    completion(.success(response))
                } catch {
                    // something when wrong while deserializing
                    completion(.failure(error))
                }

            case .failure(let error):
                // something went wrong during transmission (couldn't connect, dropped connection, etc)
                completion(.failure(error))
        }
    })
    loader.load(task)
}

} ```

使用條件化擴充套件,我們可以簡化 Request 構造:

``` extension Request where Response == Person { static func person(_ id: Int) -> Request { return Request(personID: id) }

init(personID: Int) {
    let request = HTTPRequest(path: "/api/person/(personID)/")

    // because Person: Decodable, this will use the initializer that automatically provides a JSONDecoder to interpret the response
    self.init(underlyingRequest: request)
}

}

// usage: // automatically infers Request<Person> based on the initializer/static method connection.request(Request(personID: 1)) { ... }

// or: connection.request(.person(1)) { ... } ```

這裡有一些重要的事情在起作用:

  • 請記住,即使是 404 Not Found 響應也是成功的響應。 這是我們從伺服器返回的響應! 解釋該響應是客戶端問題。 所以預設情況下,我們可以盲目地嘗試反序列化任何響應,因為每個 HTTPResponse 都是“成功”的響應。 這意味著處理 404 Not Found 或 304 Not Modified 響應取決於客戶端。
  • 通過使每個請求解碼響應,我們提供了個性化/特定於請求的反序列化邏輯的機會。 如果解碼失敗,一個請求可能會查詢 JSON 響應中編碼的錯誤,而另一個請求可能只是滿足於丟擲 DecodingError。
  • 由於每個 Request 使用閉包進行解碼,我們可以在閉包中捕獲特定於域和上下文的值,以幫助特定請求的解碼過程!
  • 我們不僅限於 JSON 反序列化。 一些請求可能反序列化為 JSON; 其他人可能會使用 XMLDecoder 或自定義的東西反序列化。 每個請求都有機會根據自己的意願解碼響應。
  • 對 Request 的條件擴充套件意味著我們有一個漂亮且富有表現力的 API connection.request(.person(42)) { ... }

Hello, Combine!

此連線層還可以輕鬆地與 Combine 整合。 我們可以在 Connection 上提供一個方法來公開發送請求並返回一個符合 Publisher 的型別,以在釋出者鏈中使用或作為 ObservableObject 的一部分,甚至在 SwiftUI 中使用 .onReceive() 修飾符:

``` import Combine

extension Connection {

// Future<...> is a Combine-provided type that conforms to the Publisher protocol
public func publisher<ResponseType>(for request: Request<ResponseType>) -> Future<ResponseType, Error> {
    return Future { promise in
        self.request(request, completion: promise)
    }
}

// This provides a "materialized" publisher, needed by SwiftUI's View.onReceive(...) modifier
public func publisher<ResponseType>(for request: Request<ResponseType>) -> Future<Result<ResponseType, Error>, Never> {
    return Future { promise in
        self.request(request, completion: { promise(.success($0)) }
    }
}

} ```

結論

我們終於走到了盡頭! 我希望您喜歡這個系列,並希望它能為您開啟思路,迎接新的可能性。 我希望你能從中學到一些東西:

  • HTTP 並不可怕、複雜。 從本質上講,它真的非常簡單。 它是一種用於傳送請求的簡單的基於文字的格式,也是一種用於獲取響應的簡單格式。 我們可以輕鬆地在 Swift 中對其進行建模。
  • 將 HTTP 抽象為高階“請求/響應”模型允許我們做一些非常酷的事情,如果我們在 HTTP 森林中檢視所有特定於 URLSession 的樹時,這些事情將很難實現。
  • 我們可以有蛋糕也可以吃吃蛋糕! 無論您使用的是 UIKit/AppKit 還是 SwiftUI 或其他任何東西,這種網路模型都能很好地工作。
  • 通過認識到我們不需要泛型或協議,我們避免了程式碼的過度複雜化。 載入程式鏈的每個部分都是離散的、可組合的,並且易於單獨測試。 在使用它時,我們永遠不必處理那些可怕的“關聯型別或自身”錯誤。
  • 無論您使用何種程式語言和平臺,這種方法的原理都適用。 本系列的主題是“如何思考問題”。 謝謝閱讀!