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