Swift中HTTP(八) 请求选项

2,464 阅读5分钟

HTTP简介

HTTP基础结构

HTTP请求体

HTTP 加载请求

HTTP 模拟测试

HTTP 链式加载器

HTTP 动态修改请求

HTTP 请求选项

HTTP 重置

HTTP 取消

HTTP 限流

HTTP 重试

HTTP 基础鉴权

HTTP 自动鉴权设置

HTTP 自动鉴权

HTTP 复合加载器

HTTP 头脑风暴

HTTP 总结

到目前为止,我们已经编写了足够多的代码来描述 HTTPLoader 实例链,这些实例可以处理传入的 HTTPRequest 并最终生成 HTTPResult

然而,在某些情况下,我们不希望每个请求都以相同的方式加载。 上次我们编写了 ApplyEnvironment,这是一个 HTTPLoader 子类,它将使用预定义的 ServerEnvironment 值来填充请求中的任何缺失值。 我们将以此作为我们的案例研究。

让我们想象一下,我们决定用星球大战 wiki“Wookieepedia”中的附加信息来补充我们的 StarWarsAPI。 当然,我们知道我们可以手动设置主机和路径以及每个请求发出时的所有内容,但最好不要这样做,你值得拥有美好的事物。

// it would be unfortunate to have to repeat this a lot
var request = HTTPRequest()
request.host = "starwars.fandom.com"
request.path = "/api/v1/Search/List"
request.queryItems = [
    URLQueryItem(name: "query", value: "anakin")
]

相反,让我们添加在请求进入链之前指定整个环境的能力,然后教 ApplyEnvironment 加载器寻找它。 也许它看起来像这样:

var request = HTTPRequest()
request.serverEnvironment = .wookieepedia
request.path = "Search/List"
request.queryItems = [
    URLQueryItem(name: "query", value: "anakin")
]

这看起来几乎和以前一样多的代码,但我相信它更具表现力。 我们正在删除更多的魔法字符串(“starwars.fandom.com”和“/api/v1”)并在我们的意图中更具描述性(“我们想要‘Wookieepedia’服务器环境”)。

我们不想做的是回到我们的 HTTPRequest 定义并为服务器环境添加一个新的存储属性。 并非每个请求都需要指定一个服务器环境,并且在每个请求上都为一个服务器环境腾出空间是一种浪费。 此外,如果我们决定要指定其他每个请求选项,则该方法不能很好地扩展。 (提示:有!)

相反,我们将在请求中定义一个私有转储场来存储这些选项,并为它们创建一个类型安全的接口。

SwiftUI 中汲取灵感

AppleSwiftUI 框架中有一个名为 PreferenceKey 的简洁小协议。 它基本上是视图在其父视图层次结构中传递类型安全的“首选项值”的一种方式,因此某些祖先可以查找并读取它。

我们将为我们的请求使用同样的东西。 我们将从一个协议开始:

public protocol HTTPRequestOption {
    associatedtype Value

    /// The value to use if a request does not provide a customized value
    static var defaultOptionValue: Value { get }
}

该协议表示“选项”只是一种具有静态 defaultOptionValue 属性的类型,如果请求未指定,我们可以使用该属性。

接下来,我们将教授 HTTPRequest 有关选项的知识:

public struct HTTPRequest {
    ...
    private var options = [ObjectIdentifier: Any]()

    public subscript<O: HTTPRequestOption>(option type: O.Type) -> O.Value {
        get {
            // create the unique identifier for this type as our lookup key
            let id = ObjectIdentifier(type)

            // pull out any specified value from the options dictionary, if it's the right type
            // if it's missing or the wrong type, return the defaultOptionValue
            guard let value = options[id] as? O.Value else { return type.defaultOptionValue }

            // return the value from the options dictionary
            return value
        }
        set {
            let id = ObjectIdentifier(type)
            // save the specified value into the options dictionary
            options[id] = newValue
        }
    }
}

这是持有期权价值的基础设施。 现在假设请求可以专门保存 ServerEnvironment 值:

public struct ServerEnvironment: HTTPRequestOption {
    // the associated type is inferred to be "Optional<ServerEnvironment>"
    public static let defaultOptionValue: ServerEnvironment? = nil
    
    ...
}

我们的 ServerEnvironment 结构,它包含默认主机、路径前缀等值,也是一个 HTTPRequestOption。 如果我们没有在请求上设置明确的 ServerEnvironment,那么请求“持有”的值是 nil(默认选项值),意思是“没有自定义的服务器环境”。

我们可以添加的一件好事是对 HTTPRequest 的扩展,以使其更易于使用:

extension HTTPRequest {
    
    public var serverEnvironment: ServerEnvironment? {
        get { self[option: ServerEnvironment.self] }
        set { self[option: ServerEnvironment.self] = newValue }
    }
    
}

有了这个,我们现在有办法在单个 HTTPRequest 上设置任意数量的自定义值,并以类型安全的方式再次检索它们。

使用选项值

剩下的最后一件事是教您的ApplyEnvironment程序如何寻找要使用的环境。 如果您还记得,该类目前看起来像这样:

public class ApplyEnvironment: HTTPLoader {

    private let environment: ServerEnvironment

    public init(environment: ServerEnvironment) {
        environment = environment
        super.init()
    }

    override public func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void) {
        var copy = request

        if copy.host.isEmpty { 
            copy.host = environment.host
        }
        if copy.path.hasPrefix("/") == false {
            // TODO: apply the environment.pathPrefix
        }
        // TODO: apply the query items from the environment
        for (header, value) in environment.headers {
            // TODO: add these header values to the request
        }

        super.load(request: copy, completion: completion)
    }
}

我们只需要对 load(request:completion:) 方法做一个简单的调整:

    override public func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void) {
        var copy = request

        // use the environment specified by the request, if it's present
        // if it doesn't have one, use the one passed to the initializer
        let requestEnvironment = request.serverEnvironment ?? environment

        if copy.host.isEmpty { 
            copy.host = requestEnvironment.host
        }
        if copy.path.hasPrefix("/") == false {
            // TODO: apply the requestEnvironment.pathPrefix
        }
        // TODO: apply the query items from the requestEnvironment
        for (header, value) in requestEnvironment.headers {
            // TODO: add these header values to the request
        }

        super.load(request: copy, completion: completion)
    }

就是这样!

我们现在为我们添加了一种通过声明选项来自定义单个请求行为的方法:一个类型安全的值,它随请求一起携带并由各种加载器检查,以便它们可以针对该特定请求动态改变它们的行为。

在以后的帖子中,我们将使用选项来自定义多种行为,包括指定应如何重试请求、它们的身份验证机制是什么(如果有)、应如何缓存响应(如果有)等等。

在我们的下一篇文章中,我们将从自定义加载器实现中退一步,看看“重置”加载器的概念。