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<Response> {
    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<Response> {
        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 或其他任何东西,这种网络模型都能很好地作业。
  • 经过认识到咱们不需求泛型或协议,咱们避免了代码的过度杂乱化。 加载程序链的每个部分都是离散的、可组合的,并且易于单独测验。 在运用它时,咱们永久不用处理那些可怕的“关联类型或本身”错误。
  • 不管您运用何种编程言语和平台,这种办法的原理都适用。 本系列的主题是“怎么思考问题”。
    谢谢阅览!