用 RxSwift + Moya 写出优雅的网络请求代码
RxSwift
Rx
是微软出品的一个 Funtional Reactive Programming
框架,RxSwift
是它的一个 Swift 版本的实现。
RxSwift 的主要目的是能简单的处理多个异步操作的组合,和事件/数据流。
利用 RxSwift,我们可以把本来要分散写到各处的代码,通过方法链式调用来组合起来,非常的好看优雅。
举个例子,有如下操作:
点击按钮 -> 发送网络请求 -> 对返回的数据进行某种格式处理 -> 显示在一个 UILabel 上
代码如下:
sendRequestButton
.rx_tap
.flatMap(viewModel.loadData)
.throttle(0.3, scheduler: MainScheduler.instance)
.map { "\($0.debugDescription)" }
.bindTo(self.resultLabel.rx_text)
.addDisposableTo(disposeBag)
是不是看上去很优雅呢?
另外这篇文章中也有一个类似的例子:
对应的代码是:
button
.rx_tap // 点击登录
.flatMap(provider.login) // 登录请求
.map(saveToken) // 保存 token
.flatMap(provider.requestInfo) // 获取用户信息
.subscribe(handleResult) // 处理结果
用一连串的链式调用就把一系列事件处理了,是不是很不错。
Moya
Moya
是 Artsy 团队的 Ash Furrow 主导开发的一个网络抽象层库。它在 Alamofire 基础上提供了一系列简单的抽象接口,让客户端代码不用去直接调用 Alamofire,也不用去关心 NSURLSession。同时提供了很多实用的功能。
它的 Target -> Endpoint -> Request
模式也使得每个请求都可以自由定制。
下面进入正题:
创建一个请求
Moya 的 TargetType
协议规定的创建网络请求的方法,用枚举来创建,很有 Swift 的风格。
enum DataAPI {
case Data
}
extension DataAPI: TargetType {
var baseURL: NSURL { return NSURL(string: "http://localhost:3000")! }
var path: String {
return "/data"
}
var method: Moya.Method {
return .GET
}
var parameters: [String : AnyObject]? {
return nil
}
var sampleData: NSData {
return stubbedResponseFromJSONFile("stub_data")
}
var multipartBody: [Moya.MultipartFormData]? {
return nil
}
}
创建数据模型
数据模型的创建用了 SwiftyJSON
和 Moya_SwiftyJSONMapper
,方便将 JSON 直接映射成 Model 对象。
struct DataModel: ALSwiftyJSONAble {
var title: String?
var content: String?
init?(jsonData: JSON) {
self.title = jsonData["title"].string
self.content = jsonData["content"].string
}
}
发送请求
我们可使用 Moya 自带一个 RxSwift 的扩展来发送请求。
class ViewModel {
private let provider = RxMoyaProvider<DataAPI>() // 创建为 RxSwift 扩展的 MoyaProvider
func loadData() -> Observable<DataModel> {
return provider
.request(.DataRequest) // 通过某个 Target 来指定发送哪个请求
.debug() // 打印请求发送中的调试信息
.mapObject(DataModel) // 请求的结果映射为 DataModel 对象
}
}
然后在 ViewController 中就可以写上面说到过的那一段了
sendRequestButton
.rx_tap // 观察按钮点击信号
.flatMap(viewModel.loadData) // 调用 loadData
.map { "\($0.title) \($0.content)" } // 格式化显示内容
.bindTo(self.resultLabel.rx_text) // 绑定到 UILabel 上
.addDisposableTo(disposeBag) // 添加到 disposeBag,当 disposeBag 释放时,这个绑定关系也会被释放
这样就实现了 点击按钮 -> 发送网络请求 -> 显示结果
上面这一段没有考虑错误处理,这个后面会说。
URL 缓存
URL 缓存则是采用 Alamofire 的缓存处理方式——用系统缓存(NSURLCache)。
NSURLCache
默认采用的缓存策略是 NSURLRequestUseProtocolCachePolicy
。
缓存的具体方式可以由服务端在返回的响应头部添加 Cache-Control
字段来控制。
离线缓存
有一种缓存是系统的缓存做不到的,就是离线缓存。
离线缓存的流程是:
发请求前先看看本地有没有离线缓存
有 -> 使用离线缓存数据渲染界面 -> 发出网络请求 -> 用请求到的数据更新界面
无 -> 发出网络请求 -> 用请求到的数据更新界面
由于 Moya 没有提供离线缓存这个功能,只能自己写了。
为 RxMoyaProvider 扩展离线缓存功能:
extension RxMoyaProvider {
func tryUseOfflineCacheThenRequest(token: Target) -> Observable<Moya.Response> {
return Observable.create { [weak self] observer -> Disposable in
let key = token.cacheKey // 缓存 Key,可以根据自己的需求来写,这里采用的是 BaseURL + Path + Parameter转化为JSON字符串
// 先读取缓存内容,有则发出一个信号(onNext),没有则跳过
if let response = HSURLCache.sharedInstance.cachedResponseForKey(key) {
observer.onNext(response)
}
// 发出真正的网络请求
let cancelableToken = self?.request(token) { result in
switch result {
case let .Success(response):
observer.onNext(response)
observer.onCompleted()
HSURLCache.sharedInstance.cacheResponse(response, forKey: key)
case let .Failure(error):
observer.onError(error)
}
}
return AnonymousDisposable {
cancelableToken?.cancel()
}
}
}
}
以上代码创建了一个信号序列,当有离线缓存时,会发出一个信号,当网络请求结果返回时,会发出一个信号,当网络请求失败时,也会发出一个错误信号。
上面的 HSURLCache 是我自己写的一个缓存类,通过 SQLite 把 Moya 的 Response 对象保存到数据库中。
由于 Moya 的 Response 对象是被 `final` 修饰的,无法通过继承方式为其添加 NSCoder 实现。所以就将 Response 的三个属性分别保存。
读缓存数据时也是读出三个属性的数据,再用他们创建成 Response 对象。
func loadData() -> Observable<DataModel> {
return provider
.tryUseOfflineCacheThenRequest(.DataRequest)
.debug()
.distinctUntilChanged()
.mapObject(DataModel)
}
使用离线缓存的网络请求方式可以写成这样,调用了上面所说的 tryUseOfflineCacheThenRequest
方法。
并且这里用了 RxSwift 的 distinctUntilChanged
方法,当两个信号完全一样时,会过滤掉后面的信号。这样避免页面在数据相同的情况下渲染两次。
错误处理
可以通过判断 event 对象来处理错误,代码如下:
sendRequestButton
.rx_tap
.flatMap(viewModel.loadData)
.throttle(0.3, scheduler: MainScheduler.instance)
.map { "\($0.title) \($0.content)" }
.subscribe { event in
switch event {
case .Next(let data):
print(data)
case .Error(let error):
print(error)
case .Completed:
break
}
}
.addDisposableTo(disposeBag)
本地假数据
这时 Moya 的一个功能,可以在本地放置一个 json 文件,网络请求可以设置成读取本地文件内容来返回数据。可以在接口故障或为开发完时,客户端可以先用假数据来开发,先走通流程。
只要在创建 RxMoyaProvider 时指定一个参数 stubClosure
。
使用本地假数据:
RxMoyaProvider<DataAPI>(stubClosure: MoyaProvider.ImmediatelyStub)
使用网络接口真实数据:
RxMoyaProvider<DataAPI>(stubClosure: MoyaProvider.NeverStub)
Moya 也提供了一个模拟网络延迟的方法。
使用本地假数据并有 3 秒的延迟:
RxMoyaProvider<DataAPI>(stubClosure: MoyaProvider.DelayedStub(3))
Header 处理
例如如果想要在 Header 中添加一些字段,例如 access-token,可以通过 Moya 的 Endpoint Closure
方式实现,代码如下:
let commonEndpointClosure = { (target: Target) -> Endpoint<Target> in
var URL = target.baseURL.URLByAppendingPathComponent(target.path).absoluteString
let endpoint = Endpoint<Target>(URL: URL,
sampleResponseClosure: {.NetworkResponse(200, target.sampleData)},
method: target.method,
parameters: target.parameters)
// 添加 AccessToken
if let accessToken = currentUser.accessToken {
return endpoint.endpointByAddingHTTPHeaderFields(["access-token": accessToken])
} else {
return endpoint
}
}
插件机制
另外 Moya 的插件机制也很好用,提供了两个接口,willSendRequest
和 didReceiveResponse
,可以在请求发出前和请求收到后做一些额外的处理,并且不和主功能耦合。
Moya 本身提供了打印网路请求日志的插件和 NetworkActivityIndicator 的插件。
例如检测 access-token 的合法性:
internal final class AccessTokenPlugin: PluginType {
func willSendRequest(request: RequestType, target: TargetType) {
}
func didReceiveResponse(result: Result<RxMoya.Response, RxMoya.Error>, target: TargetType) {
switch result {
case .Success(let response):
do {
let jsonObject = try response.mapJSON()
let json = JSON(jsonObject)
if json["status"].intValue == InvalidStatus {
NSNotificationCenter.defaultCenter().postNotificationName("InvalidTokenNotification", object: nil)
}
} catch {
}
case .Failure(_):
break
}
}
}
然后在创建 RxMoyaProvider 时注册插件:
private let provider = RxMoyaProvider<DataAPI>(stubClosure: MoyaProvider.NeverStub, plugins: [AccessTokenPlugin()])
结语
对于用 Swift 编写的项目来说,可以有比 Objective-C 更优雅的方式来编写网络层代码。RxSwift + Moya 是个不错的选择,不仅能使代码更优雅美观,方便维护,还有具有一些很实用的小功能。