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 或其他任何东西,这种网络模型都能很好地工作。
- 通过认识到我们不需要泛型或协议,我们避免了代码的过度复杂化。 加载程序链的每个部分都是离散的、可组合的,并且易于单独测试。 在使用它时,我们永远不必处理那些可怕的“关联类型或自身”错误。
- 无论您使用何种编程语言和平台,这种方法的原理都适用。 本系列的主题是“如何思考问题”。 谢谢阅读!