最近因为项目需要,又给我写的网络框架添加了不少功能。到目前看来,这个网络框架已经具备了最基本的项目需求,实现了以后大部分项目都能使用的现实需要,但还有几点令我不太满意。这篇文章就是记录我设计这个框架的整体思路,以及记录一下不满意的地方,方便以后回顾修改。
构建一个请求
首先,我的设计思路是偏向 POP 的,也就是面向协议编程。框架并不直接提供 URLRequest 的构建接口,而是由继承了 Requestable
协议的类与结构体自身来实现。
Requestable
协议是长这个样子的:
public protocol Requestable {
associatedtype AnyResponse: Response
associatedtype AnyRequest: Request
var request: AnyRequest { get set }
var response: AnyResponse.Type { get set }
var extendErrorHandler: ((Data) -> Error?)? { get }
var logRequestTime: Bool { get }
}
可以看到,这其中还有两个关联类型,它们分别是负责描述请求的 Request 协议。负责描述回调的 Response 协议。
它们的结构是这样的:
public protocol Request {
var url: URL { get set }
var httpMethod: String { get set }
var body: Data? { get set }
var header: [String: String]? { get set }
var type: RequestType { get set }
var cachePolicy: CachePolicy { get set }
}
public protocol Response: JSON {
associatedtype Data
var code: Int? { get set }
var msg: String? { get set }
var data: Data? { get set }
}
而 Reponse 又要求实现 JSON 协议,JSON 协议其实比较特殊,它规范了你的数据模型必须是这样的:
public protocol JSON {
var makeJSON: [String: Any]? { get }
static func makeModel(_ deserielized: String?) -> Self?
static func makeModel(_ deserielized: [String: Any]?) -> Self?
}
也就是,你需要提供模型转字典、字典转模型这一基本能力,而这项操作是相对复杂且多元化的,有非常多的第三方框架能帮助实现这一点,甚至自己写也不麻烦,所以我索性将 JSON 协议设计为“可变实现”的协议,这充分利用了 Swift 的协议特性。什么意思呢,原理其实很简单,首先我定义一个类型别名,也就是 typealias,它的内容是“任意一个能实现字典模型互转的类型(简称类型 M) & JSON”,假定这个别名取为 Model,那么,只要我全局实现 Model,那么就等于实现了 JSON,且依赖于类型 M,我能让所有 Model 都实现字典与模型互转。
具体做起来也很简单,首先我创建一个类型别名:
public typealias Model = JSON & HandyJSON
这里我使用 HandyJSON 来作为类型 M 的示例,接下来,我需要将 HandyJSON 提供的字典模型互转方法作为 JSON 的默认实现,方法如下:
public extension JSON where Self: HandyJSON {
var makeJSON: [String: Any]? {
toJSON()
}
static func makeModel(_ deserielized: String?) -> Self? {
deserialize(from: deserielized)
}
static func makeModel(_ deserielized: [String: Any]?) -> Self? {
deserialize(from: deserielized)
}
}
此时,只要类型是 Model 的结构和类,都会拥有字典模型互转的能力了。最后,我们再赋予 JSON 一个默认实现:
public extension JSON {
var makeJSON: [String: Any]? {
nil
}
static func makeModel(_ deserielized: String?) -> Self? {
nil
}
static func makeModel(_ deserielized: [String: Any]?) -> Self? {
nil
}
}
这一点主要是方便于其它没有实现 HandyJSON 但又想实现 JSON 以使用网络框架的结构和类。
而我为什么要大张旗鼓地做到这一点呢,主要是考虑到了 HandyJSON 的不稳定性,HandyJSON 在最近的几次 Swift 语言更新中,都体现出了不稳定性,很容易就导致 App 崩溃,如果遇到了这种情况,甚至 Swift 或 iOS 发生了一些 HandyJSON 无法解决的更新,HandyJSON 用就无法使用,那么我只需要修改 Model 的定义,并提供新的默认实现,就能拯救我的 App,而无需去修改每一个文件,使得去除 HandyJSON。
然后,我给 Requestable 提供一个默认方法 resume,这个方法用于返回一个可订阅的信号源,只要订阅了这个信号,就能获得这个信号的状态,Rx 对这些状态做了很好的区分,你可以选择性的订阅想要的信号类型,例如成功、失败、完成状态。
resume 的实现是这样的:
func resume() -> Observable<AnyResponse?> {
ObservableCreate(requestable: self).resume()
}
struct ObservableCreate<R: Requestable> {
var requestable: R
func resume() -> Observable<R.AnyResponse?> {
let network = Network(requestable)
let url = requestable.request.url
var request = URLRequest(url: url)
request.httpMethod = requestable.request.httpMethod
request.httpBody = requestable.request.body
request.timeoutInterval = NetworkConfiguration.timeoutInterval
network.cachePolicy = requestable.request.cachePolicy
requestable.request.header?.forEach({ (key, value) in
request.setValue(value, forHTTPHeaderField: key)
})
network.urlRequest = request
network.errorHandler = requestable.extendErrorHandler
return equestable.request.type.resume(network)
}
}
通过 ObservableCreate 来统一创建这个信号,那么你可能已经发现了 Network 这个类,这个类就是框架的核心部分,实际的对请求完成构建、发送、回调处理、缓存等操作,同时也是对 Alamofire 的一个封装。
除此之外,Requestable 还有两个额外属性,它们是:
var extendErrorHandler: ((Data) -> Error?)? { get }
var logRequestTime: Bool { get }
这两个属性的 get 方法是有默认实现的,也就是 Requestable 不强制你实现它们,但依然是构建一个请求较为重要的部分。
extendErrorHandler 是一个闭包,你可以在这里拿到所有请求回调的数据,但我不建议你在这里对数据进行操作,这里指应该关心回调中的错误信息,也就是说,此时请求是成功的,但服务器可能返回的不是一个标准的数据,或直接包装了 Error 数据模型返回。如果在这里返回了一个 Error,那么请求将不会被标记为成功,最终会走到错误回调中,也就是 onError 事件。
logRequestTime 默认是 false,它的作用是用来记录每次请求的时间,并且会把日志输出到一个磁盘文件中,便于接口优化。
那么现在,我们就可以动手构建一个请求了,大概是这样的:
struct BindUser: Requestable {
var request: BindUserRequest = .init()
var response: BindUserResponse.Type = BindUserResponse.self
}
struct BindUserRequest: Request {
static var path: String {
#if DEBUG
return "http://debug.com”
#else
return "http://release.com”
#endif
}
var cachePolicy: CachePolicy = .withoutCache
var url: URL = URL.init(string: “\(BindUserRequest.path)/api“)!
var httpMethod: String = "POST"
var body: Data?
var header: [String : String]? = [“Content-Type”: “application/x-www-form-urlencoded”]
var type: RequestType = .request
let appToken: String = "API"
init() {
}
}
struct BindUserResponse: Response {
var code: Int?
var msg: String?
var data: String?
}
struct BindUserModel: Model {
var status: String?
var message: String?
var data: String?
}
这是一个用于绑定用户信息的后台接口,在实现了这个接口的定义后,我就可以在需要用到的地方进行调用:
BindUser().resume()
最后,通过对 resume 返回的信号源进行订阅,我就可以对数据进行处理了。
订阅一个请求
前文提到,该框架实际上就是一个对 RxSwift + Alamofire 的封装,所以当请求发出后,你可以立刻对请求进行订阅,框架会对数据进行二次封装,然后通过 RxSwift 这一层对数据进行筛选。
数据处理的结果,符合 RxSwift 对数据的定义,也就是说:
onNext 信号:请求成功且无任何错误回调。
onError 信号:请求发生了任何错误,包括下层应用层错误和上层返回了自定义错误。
onComplete 信号:请求完成了,包括请求成功和请求失败。
现在就可以根据需要来对数据进行筛选,例如,我只关心请求是否结束,我不关心成功与失败,那么只需要订阅 onComplete 信号就足够了。
// 在界面上显示一个“正在请求”的图标。
Alert.show()
_ = BindUser().resume().subscribe(onComplete: {
// 请求完成了,不论成功或失败,都应该隐藏“正在请求”的图标。
Alert.hide()
})
缓存
框架的缓存策略是基于请求域名+相对路径的,也就是不论 GET 参数是什么、协议头是什么,都不会影响到缓存机制。
在实现 Request 协议时,或者手动创建 Network 对象时,你就可以指定一个缓存策略,目前的缓存策略有如下几种:
/// 始终使用缓存,如果没有缓存,则在第一次请求成功时建立缓存。
case alwaysUseCache
/// 首先使用缓存,当请求成功后,会发出请求的数据,并覆盖原缓存。
case useCacheAndRequest
/// 使用缓存,当请求成功后,会发出请求的数据,但不覆盖原有缓存。
case useCacheAndRequestWithoutRecache
/// 默认。始终不使用缓存,请求的数据也不会缓存到本地。
case withoutCache
到目前,框架还不会对非 JSON 串的二进制数据进行缓存,例如图片、视频等,所以需要依赖其他方式。
缓存的底层实现是利用了 SQLite 数据库,在需要的时候直接将数据写入数据库,或从数据库读取缓存的数据。
目前的缓存策略还非常的原始,以后我需要为它添加更多能力,例如最大缓存大小、清除缓存、缓存过期时间,同时还需要能够缓存二进制文件,能识别这个二进制文件在服务器上是否发生了修改,如果修改了,则要建立新的缓存,如果没有修改,那么直接使用缓存即可。
最后
对框架不满意的部分,其实已经体现在上文中了。包括:
框架是对 Alamofire 的二次封装,依赖项太庞大了,需要一个轻量化的,基于原生 URLRequest 的封装。
框架的缓存机制还非常原始。
框架的性能还未在考虑之中。
源码可能会在我实现了全面的缓存机制之后传到 GitHub 上。