在开发泊学 App 服务端的时候,我们提到过 resource 的概念。简单来说,resource 就是通过 HTTP API 进行增删改查的一个目标。通常,这个目标对应着一个或多个 model。这一节,我们在客户端也把要发起的网络请求封装成 resource,一方面,这会让代码的表现力更好;另一方面,就像在这个系列一开始提到的,网络通信作为一种获取数据的途径,从功能上说和从本地存储数据没什么区别,于是相关的IO代码也不应该过于显现地出现在代码里。

从一个最简单的封装说起

对网络IO最简单的封装,通常都是通过 URLSession 的扩展提供一个方法,这个方法把网络通信的部分封装起来,让我们只要提供访问的 URL 以及相应的数据处理方法就好了。于是,一个“跟着感觉走”写出来的代码就是这样的:

extension URLSession {
  func load<T>(
    _ url: URL,
    parser: @escaping (Data) throws -> T,
    callback: @escaping (Swift.Result<T, Error>) -> ()) {
    dataTask(with: url) {
      data, response, error in
      callback(Swift.Result {
        if let e = error { throw e }
        guard let d = data else { /* throw an error indicating data corrupted. */ }

        return try parser(d)
      })
    }.resume()
  }
}

其中:

  • URL 表示请求的 HTTP API;
  • parser 表示把 Data 转换成目标类型 T 的解析函数,用户通过它注入自己期望的处理逻辑;
  • callback 则用于封装 URLSession 原生的回调函数,利用泛型,我们抽象了 URLSession 使用的一般模式 (既:有错就提取错误,没错就执行正常处理函数);

完成后,我们就可以像这样使用 load 方法了:

URLSession.shared.load(
  resourceURL,
  parser: { data in
    /* Decode and return data here. */
  }) {
    /* Handle data or error in `Result<T, Error>`. */
  }

相比最原始的把解析 Data 和业务处理的逻辑混在一起,显然这样要更清楚一些。不过,从最终成型的代码看,这还是太过于面向过程了,本质上,还是要给 URLSession 准备好 URL 和回调函数。因此,在表达“加载目标资源”这个语义上,仍旧略显不足。

通过 Resource 进一步封装

而造成这种表现力不足的核心原因,就是要访问的 URL 以及对应的处理方法用分开的方式暴露给了 API。但通常,这两个元素几乎总是同时出现的,如果我们把它们封装成一个独立的类型,这个 API 用起来,就会更有“加载资源”的语义了。

为此,我们可以单独定义一个表示“资源”的类型:

public struct Resource<R> {
  public let url: URL
  public let parser: (Data) throws -> R
}

这个类型,就是用获取资源的 URL 以及解析资源的 parser 函数构成的。接下来,我们让之前的 load 方法接受 Resource 类型的参数:

public func load<T>(
  _ r: Resource<T>,
  callback: @escaping (Swift.Result<T, Error>) -> ()) {
  dataTask(with: r.url) {
    data, response, error in
    callback(Swift.Result {
      if let e = error { throw e }
      guard let d = data else { /* throw an error indicating data corrupted. */ }

      return try r.parser(d)
    })
  }.resume()
}

于是,load 的调用就变成了这样:

let resource = Resource<someType>(url: "...", parser: {
    ...
  })

URLSession.shared.load(resource) {
    /* Handle data or error in `Result<T, Error>`. */
  }

现在,看上去就舒服多了。另外,如果 R 是一个实现了 Decodable 的类型,我们还可以给 Resource 定义一个扩展:

extension Resource where R: Decodable {
  public init(json url: URL, decoder: JSONDecoder = JSONDecoder()) {
    self.url = url
    self.parser = {
      try decoder.decode(R.self, from: $0)
    }
  }
}

这样,Resource 自身的定义也会更加直接,例如:

let pubKeyResource =
  Resource<PublicKey>(json: URL(string: "\(host)/public-key")!)

再把它和之前定义的 load 搭配在一起使用:

URLSession.shared.load(pubKeyResource) {
    /* Handle data or error in `Result<PublicKey, Error>`. */
  }

现在,基本上就没有了“面向 URLSession”的用法去编写代码的感觉,单从代码的字面含义就可以理解,这是加载公钥之后,进行某种处理。而对于不同的资源,我们只要创建对应的 Resource 对象来表达就好了。

What's next?

至此,我们就已经有了一个完全可用的 URLSession 封装了。不过,对 URLSession 的改造还没有结束。一个比较严重的不足就是 load 还在使用回调函数处理数据,这很容易由于后续的处理逻辑而导致回调函数的嵌套。因此,下一节,基于这个实现方案,我们通过 PromiseKit 进一步改进 load 的实现。

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

¥ 59

按月订阅

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

开始订阅

¥ 512

按年订阅

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

开始订阅

¥ 1280

泊学终身会员

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

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