在这篇文章中,我们将创建一个 HTTPLoader
子类,它允许我们动态修改请求。
我们已经看到 HTTPLoader
接口有非常宽松的要求“一个请求进来,一个完成块被执行”。 我们还看到了如何将 API 责任的一部分委托给其他加载器,从而允许我们创建加载器“链”。
让我们仔细看看上次声明的 PrintLoader
:
public class PrintLoader: HTTPLoader {
override func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void) {
print("Loading (request)")
super.load(request: request, completion: { result in
print("Got result: (result)")
completion(result)
})
}
}
敏锐的观察者会看到我们在将完成块发送到下一个加载器之前修改了它。 是的,我们仍在调用原始的完成处理程序,但发送的闭包在技术上与我们收到的闭包不同。 相反,我们创建了一个新的闭包并使用了那个闭包。
同样的想法也可以应用于请求。 HTTPLoader
接口没有任何关于强制我们收到的请求必须与我们发送的请求相同的要求。 我们可以完全自由地修改(甚至替换)请求,然后再将其发送到链下。 让我们运行这个想法,看看它能把我们带到哪里。
修改请求可以被认为是一个专门的“映射”函数,其中输入和输出类型都是 HTTPRequest
值。 将其建模为应用于每个请求的闭包似乎是一种自然的方式,使用它的加载器可能如下所示:
public class ModifyRequest: HTTPLoader {
private let modifier: (HTTPRequest) -> HTTPRequest
public init(modifier: @escaping (HTTPRequest) -> HTTPRequest) {
self.modifier = modifier
super.init()
}
override public func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void) {
let modifiedRequest = modifier(request)
super.load(request: modifiedRequest, completion: completion)
}
}
为什么这很有用可能不是很明显,所以让我们回想一下我们的 StarWarsAPI
类:
public class StarWarsAPI {
private let loader: HTTPLoader
public init(loader: HTTPLoader) {
self.loader = loader
}
public func requestPeople(completion: @escaping (...) -> Void) {
var r = HTTPRequest()
r.host = "swapi.dev"
r.path = "/api/people"
loader.load(request: r) { result in
// TODO: interpret the result
completion(...)
}
}
}
目前这非常简单,但您可以想象,如果我们添加更多方法来请求飞船、行星或机器人(尤其是我们正在寻找的机器人)或武器,那么我们最终会得到很多重复的 代码。 具体来说,我们会一遍又一遍地看到这段代码:
var r = HTTPRequest()
r.host = "swapi.dev"
r.path = "/api/..."
因为我们现在有办法修改飞行中的请求,所以我们不需要在任何地方重复这段代码。 让我们修改初始化程序来构建一个加载链:
public init(loader: HTTPLoader) {
let modifier = ModifyRequest { request in
var copy = request
if copy.host.isEmpty {
copy.host = "swapi.dev"
}
if copy.path.hasPrefix("/") == false {
copy.path = "/api/" + copy.path
}
return copy
}
self.loader = modifier --> loader
}
我们的 ModifyRequest
加载器将查看传入的请求并填写缺失的信息。 如果请求没有主机,它将提供一个。 如果请求的 .path 不是绝对路径,它会为我们添加一个默认路径。
现在,我们的 requestPeople 方法可以变得更简单:
public func requestPeople(completion: @escaping (...) -> Void) {
loader.load(request: HTTPRequest(path: "people")) { result in
// TODO: interpret the result
completion(...)
}
}
随着我们添加更多的request...
方法,每个方法的实现将只包含该实现独有的部分。 填写请求所需的所有通用样板文件都将由 ModifyRequest
加载程序处理。
服务器环境
我们可以通过形式化某些类型的请求修改来更进一步。 我们上面显示的是一般的“更改任何内容”修改。 我们倾向于对请求进行的最常见的一种修改集中在针对不同的后端。 我们将拥有一组用于开发的服务器,另一组用于测试,另一组用于暂存到上线,然后是生产服务器。
能够指定这些“环境”的各种细节而不必直接对转换进行硬编码非常有用。 ServerEnvironment
结构可能看起来像这样:
public struct ServerEnvironment {
public var host: String
public var pathPrefix: String
public var headers: [String: String]
public var query: [URLQueryItem]
public init(host: String, pathPrefix: String = "/", headers: [String: String] = [:], query: [URLQueryItem] = []) {
// make sure the pathPrefix starts with a /
let prefix = pathPrefix.hasPrefix("/") ? "" : "/"
self.host = host
self.pathPrefix = prefix + pathPrefix
self.headers = headers
self.query = query
}
}
使用这样的结构,我们可以预先定义众所周知的服务器环境,或使用动态检索的环境列表,或两者兼而有之。
extension ServerEnvironment {
public static let development = ServerEnvironment(host: "development.example.com", pathPrefix: "/api-dev")
public static let qa = ServerEnvironment(host: "qa-1.example.com", pathPrefix: "/api")
public static let staging = ServerEnvironment(host: "api-staging.example.com", pathPrefix: "/api")
public static let production = ServerEnvironment(host: "api.example.com", pathPrefix: "/api")
}
将此 ServerEnvironment
值应用于 HTTPRequest
是对上述闭包的扩展。 我们将测试传入 HTTPRequest
的各个部分并填写缺失的部分。 我们使用它的加载器看起来像这样:
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)
}
}
在请求通过链时修改请求是一个非常强大的工具。 我们可以用它来填充标头(User-Agent?每个请求的标识符?),检查和更新主体,将请求重定向到其他地方等等。 我们将在以后的帖子中看到更多。
在我们的下一篇文章中,我们将构建一个功能,允许单个请求提供自定义的每个请求“选项”,以改变它们的加载行为,使其远离链的默认行为。