用 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
    }
}

创建数据模型

数据模型的创建用了 SwiftyJSONMoya_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 的插件机制也很好用,提供了两个接口,willSendRequestdidReceiveResponse,可以在请求发出前和请求收到后做一些额外的处理,并且不和主功能耦合。

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 是个不错的选择,不仅能使代码更优雅美观,方便维护,还有具有一些很实用的小功能。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容