这一节,结合泊学 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 回调函数的三个参数的类型:

  • 如果 tu 都不为 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 启动时,从服务器获取公钥的代码。

所有订阅均支持 12 期免息分期

¥ 59

按月订阅

一个月,观看并下载所有视频内容。初来泊学,这可能是个最好的开始。

开始订阅

¥ 512

按年订阅

一年的时间,让我们一起疯狂地狩猎知识吧。比按月订阅优惠 28%

开始订阅

¥ 1280

泊学终身会员

永久观看和下载所有泊学网站视频,并赠送 100 元商店优惠券。

我要加入
如需帮助,欢迎通过以下方式联系我们