在开发泊学 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
的实现。