Swift中的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 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
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 或其他任何東西,這種網路模型都能很好地工作。
- 通過認識到我們不需要泛型或協議,我們避免了程式碼的過度複雜化。 載入程式鏈的每個部分都是離散的、可組合的,並且易於單獨測試。 在使用它時,我們永遠不必處理那些可怕的“關聯型別或自身”錯誤。
- 無論您使用何種程式語言和平臺,這種方法的原理都適用。 本系列的主題是“如何思考問題”。 謝謝閱讀!