这一节,结合泊学 App 中的一部分实现,我们来看如何基于 PromiseKit
进一步改善通过网络获取资源的代码。
最终,我们要实现的 load
,它的签名看上去是这样的:
public func load<T: Decodable>(_ r: Resource<T>) -> Promise<T>
也就是说,用一个 Promise
包装了通过网络获取的数据 (或者发生的错误),这样就可以用串联的方式进行各种后续处理了,也就避免了回调函数嵌套的问题。
想法倒是挺好,然而该怎么做呢?
我们先定义一个辅助函数,它把 Promise.Resolver
配接成 URLSession
要求的回调函数:
private func adapter<T, U>(
_ seal: Resolver<(data: T, response: U)>)
-> (T?, U?, Error?) -> Void {
return {
t, u, e in
if let t = t, let u = u {
seal.fulfill((t, u))
}
else if let e = e {
seal.reject(e)
}
else {
seal.reject(PMKError.invalidCallingConvention)
}
}
}
其中的 T?
,U?
和 Error?
分别对应着 URLSession.dataTask
回调函数的三个参数的类型:
- 如果
t
和u
都不为nil
,表示请求成功了,我们就把(t, u)
包装在返回的Promise
里; - 如果
error
不为nil
,就把对应的错误包装在返回的Promise
里; - 如果不是上面这些情况,我们就把
PMKError.invalidCallingConvention
包装在返回的Promise
里,这是定义在 PromiseKit 中的错误,表示调用不符合URLSession.dataTask
要求的回调函数的签名约定;
有了 adapter
之后,我们就可以像下面这样给 URLSession
添加一个扩展,创建一个返回 Promise
版本的 dataTask
方法了:
public enum PMKTag {
case promise
}
extension URLSession {
func dataTask(_ tag: PMKTag, with url: URL)
-> Promise<(data: Data, response: URLResponse)> {
return Promise { resolver in
dataTask(with: url, completionHandler: adapter(resolver)).resume()
}
}
}
这里,第一个参数 tag
,没有实际用途,它只用于说明这个 dataTask
是基于 PromiseKit 的。 完成后,就可以基于这个方法定义一开始提到的 load
方法了:
extension URLSession {
public func load<T: Decodable>(_ r: Resource<T>) -> Promise<T> {
return dataTask(.promise, with: r.url)
.map { (data, response) in
try r.parser(data)
}
}
}
现在,通过 HTTP API 获取数据的代码写出来就是这样的:
let pubKeyResource = Resource<PublicKey>(
json: URL(string: "\(host)/public-key")!)
URLSession.shared.load(pubKeyResource)
之后,就可以在 load
后面直接串联上处理 PublicKey
的代码了。
What's next?
了解了整个封装思路之后,下一节,基于这个最终改造好的 load
,我们过一遍 App 启动时,从服务器获取公钥的代码。