用 RxSwift + Moya 寫出優(yōu)雅的網(wǎng)絡(luò)請(qǐng)求代碼

RxSwift

Rx 是微軟出品的一個(gè) Funtional Reactive Programming 框架,RxSwift 是它的一個(gè) Swift 版本的實(shí)現(xiàn)摇锋。
RxSwift 的主要目的是能簡(jiǎn)單的處理多個(gè)異步操作的組合丹拯,和事件/數(shù)據(jù)流。
利用 RxSwift乱投,我們可以把本來(lái)要分散寫到各處的代碼咽笼,通過(guò)方法鏈?zhǔn)秸{(diào)用來(lái)組合起來(lái),非常的好看優(yōu)雅戚炫。

舉個(gè)例子剑刑,有如下操作:
點(diǎn)擊按鈕 -> 發(fā)送網(wǎng)絡(luò)請(qǐng)求 -> 對(duì)返回的數(shù)據(jù)進(jìn)行某種格式處理 -> 顯示在一個(gè) UILabel 上

代碼如下:

sendRequestButton
    .rx_tap
    .flatMap(viewModel.loadData)
    .throttle(0.3, scheduler: MainScheduler.instance)
    .map { "\($0.debugDescription)" }
    .bindTo(self.resultLabel.rx_text)
    .addDisposableTo(disposeBag)

是不是看上去很優(yōu)雅呢?

另外這篇文章中也有一個(gè)類似的例子:

對(duì)應(yīng)的代碼是:

button
    .rx_tap // 點(diǎn)擊登錄
    .flatMap(provider.login) // 登錄請(qǐng)求
    .map(saveToken) // 保存 token
    .flatMap(provider.requestInfo) // 獲取用戶信息
    .subscribe(handleResult) // 處理結(jié)果

用一連串的鏈?zhǔn)秸{(diào)用就把一系列事件處理了,是不是很不錯(cuò)施掏。

Moya

Moya 是 Artsy 團(tuán)隊(duì)的 Ash Furrow 主導(dǎo)開(kāi)發(fā)的一個(gè)網(wǎng)絡(luò)抽象層庫(kù)钮惠。它在 Alamofire 基礎(chǔ)上提供了一系列簡(jiǎn)單的抽象接口,讓客戶端代碼不用去直接調(diào)用 Alamofire七芭,也不用去關(guān)心 NSURLSession素挽。同時(shí)提供了很多實(shí)用的功能。
它的 Target -> Endpoint -> Request 模式也使得每個(gè)請(qǐng)求都可以自由定制狸驳。

下面進(jìn)入正題:

創(chuàng)建一個(gè)請(qǐng)求

Moya 的 TargetType 協(xié)議規(guī)定的創(chuàng)建網(wǎng)絡(luò)請(qǐng)求的方法预明,用枚舉來(lái)創(chuàng)建,很有 Swift 的風(fēng)格耙箍。

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

創(chuàng)建數(shù)據(jù)模型

數(shù)據(jù)模型的創(chuàng)建用了 SwiftyJSONMoya_SwiftyJSONMapper撰糠,方便將 JSON 直接映射成 Model 對(duì)象。

struct DataModel: ALSwiftyJSONAble {
    
    var title: String?
    var content: String?
    
    init?(jsonData: JSON) {
        self.title = jsonData["title"].string
        self.content = jsonData["content"].string
    }
}

發(fā)送請(qǐng)求

我們可使用 Moya 自帶一個(gè) RxSwift 的擴(kuò)展來(lái)發(fā)送請(qǐng)求辩昆。

class ViewModel {
    
    private let provider = RxMoyaProvider<DataAPI>() // 創(chuàng)建為 RxSwift 擴(kuò)展的 MoyaProvider
    
    func loadData() -> Observable<DataModel> {
        return provider
            .request(.DataRequest) // 通過(guò)某個(gè) Target 來(lái)指定發(fā)送哪個(gè)請(qǐng)求
            .debug() // 打印請(qǐng)求發(fā)送中的調(diào)試信息
            .mapObject(DataModel) // 請(qǐng)求的結(jié)果映射為 DataModel 對(duì)象
    }
}

然后在 ViewController 中就可以寫上面說(shuō)到過(guò)的那一段了

sendRequestButton
    .rx_tap // 觀察按鈕點(diǎn)擊信號(hào)
    .flatMap(viewModel.loadData) // 調(diào)用 loadData
    .map { "\($0.title) \($0.content)" } // 格式化顯示內(nèi)容 
    .bindTo(self.resultLabel.rx_text) // 綁定到 UILabel 上
    .addDisposableTo(disposeBag) // 添加到 disposeBag阅酪,當(dāng) disposeBag 釋放時(shí),這個(gè)綁定關(guān)系也會(huì)被釋放

這樣就實(shí)現(xiàn)了 點(diǎn)擊按鈕 -> 發(fā)送網(wǎng)絡(luò)請(qǐng)求 -> 顯示結(jié)果
上面這一段沒(méi)有考慮錯(cuò)誤處理汁针,這個(gè)后面會(huì)說(shuō)术辐。

URL 緩存

URL 緩存則是采用 Alamofire 的緩存處理方式——用系統(tǒng)緩存(NSURLCache)。
NSURLCache 默認(rèn)采用的緩存策略是 NSURLRequestUseProtocolCachePolicy施无。
緩存的具體方式可以由服務(wù)端在返回的響應(yīng)頭部添加 Cache-Control 字段來(lái)控制辉词。

離線緩存

有一種緩存是系統(tǒng)的緩存做不到的,就是離線緩存帆精。
離線緩存的流程是:
發(fā)請(qǐng)求前先看看本地有沒(méi)有離線緩存
有 -> 使用離線緩存數(shù)據(jù)渲染界面 -> 發(fā)出網(wǎng)絡(luò)請(qǐng)求 -> 用請(qǐng)求到的數(shù)據(jù)更新界面
無(wú) -> 發(fā)出網(wǎng)絡(luò)請(qǐng)求 -> 用請(qǐng)求到的數(shù)據(jù)更新界面

由于 Moya 沒(méi)有提供離線緩存這個(gè)功能较屿,只能自己寫了。
為 RxMoyaProvider 擴(kuò)展離線緩存功能:

extension RxMoyaProvider {
    func tryUseOfflineCacheThenRequest(token: Target) -> Observable<Moya.Response> {
        return Observable.create { [weak self] observer -> Disposable in
            let key = token.cacheKey // 緩存 Key卓练,可以根據(jù)自己的需求來(lái)寫隘蝎,這里采用的是 BaseURL + Path + Parameter轉(zhuǎn)化為JSON字符串
            
            // 先讀取緩存內(nèi)容,有則發(fā)出一個(gè)信號(hào)(onNext)襟企,沒(méi)有則跳過(guò)
            if let response = HSURLCache.sharedInstance.cachedResponseForKey(key) {
                observer.onNext(response)
            }
            
            // 發(fā)出真正的網(wǎng)絡(luò)請(qǐng)求
            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()
            }
        }
    }
}

以上代碼創(chuàng)建了一個(gè)信號(hào)序列嘱么,當(dāng)有離線緩存時(shí),會(huì)發(fā)出一個(gè)信號(hào)顽悼,當(dāng)網(wǎng)絡(luò)請(qǐng)求結(jié)果返回時(shí)曼振,會(huì)發(fā)出一個(gè)信號(hào),當(dāng)網(wǎng)絡(luò)請(qǐng)求失敗時(shí)蔚龙,也會(huì)發(fā)出一個(gè)錯(cuò)誤信號(hào)冰评。

上面的 HSURLCache 是我自己寫的一個(gè)緩存類,通過(guò) SQLite 把 Moya 的 Response 對(duì)象保存到數(shù)據(jù)庫(kù)中木羹。  
由于 Moya 的 Response 對(duì)象是被 `final` 修飾的甲雅,無(wú)法通過(guò)繼承方式為其添加 NSCoder 實(shí)現(xiàn)解孙。所以就將 Response 的三個(gè)屬性分別保存。  
讀緩存數(shù)據(jù)時(shí)也是讀出三個(gè)屬性的數(shù)據(jù)抛人,再用他們創(chuàng)建成 Response 對(duì)象弛姜。
func loadData() -> Observable<DataModel> {
    return provider
        .tryUseOfflineCacheThenRequest(.DataRequest)
        .debug()
        .distinctUntilChanged()
        .mapObject(DataModel)
}

使用離線緩存的網(wǎng)絡(luò)請(qǐng)求方式可以寫成這樣,調(diào)用了上面所說(shuō)的 tryUseOfflineCacheThenRequest 方法妖枚。
并且這里用了 RxSwift 的 distinctUntilChanged 方法廷臼,當(dāng)兩個(gè)信號(hào)完全一樣時(shí),會(huì)過(guò)濾掉后面的信號(hào)绝页。這樣避免頁(yè)面在數(shù)據(jù)相同的情況下渲染兩次荠商。

錯(cuò)誤處理

可以通過(guò)判斷 event 對(duì)象來(lái)處理錯(cuò)誤,代碼如下:

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)

本地假數(shù)據(jù)

這時(shí) Moya 的一個(gè)功能续誉,可以在本地放置一個(gè) json 文件结啼,網(wǎng)絡(luò)請(qǐng)求可以設(shè)置成讀取本地文件內(nèi)容來(lái)返回?cái)?shù)據(jù)∏撸可以在接口故障或?yàn)殚_(kāi)發(fā)完時(shí),客戶端可以先用假數(shù)據(jù)來(lái)開(kāi)發(fā)朴译,先走通流程井佑。

只要在創(chuàng)建 RxMoyaProvider 時(shí)指定一個(gè)參數(shù) stubClosure

使用本地假數(shù)據(jù):

RxMoyaProvider<DataAPI>(stubClosure: MoyaProvider.ImmediatelyStub)

使用網(wǎng)絡(luò)接口真實(shí)數(shù)據(jù):

RxMoyaProvider<DataAPI>(stubClosure: MoyaProvider.NeverStub)

Moya 也提供了一個(gè)模擬網(wǎng)絡(luò)延遲的方法眠寿。
使用本地假數(shù)據(jù)并有 3 秒的延遲:

RxMoyaProvider<DataAPI>(stubClosure: MoyaProvider.DelayedStub(3))

Header 處理

例如如果想要在 Header 中添加一些字段躬翁,例如 access-token,可以通過(guò) Moya 的 Endpoint Closure 方式實(shí)現(xiàn)盯拱,代碼如下:

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

插件機(jī)制

另外 Moya 的插件機(jī)制也很好用盒发,提供了兩個(gè)接口,willSendRequestdidReceiveResponse狡逢,可以在請(qǐng)求發(fā)出前和請(qǐng)求收到后做一些額外的處理宁舰,并且不和主功能耦合。

Moya 本身提供了打印網(wǎng)路請(qǐng)求日志的插件和 NetworkActivityIndicator 的插件奢浑。

例如檢測(cè) 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
        }
    }
}

然后在創(chuàng)建 RxMoyaProvider 時(shí)注冊(cè)插件:

private let provider = RxMoyaProvider<DataAPI>(stubClosure: MoyaProvider.NeverStub, plugins: [AccessTokenPlugin()])

結(jié)語(yǔ)

對(duì)于用 Swift 編寫的項(xiàng)目來(lái)說(shuō)蛮艰,可以有比 Objective-C 更優(yōu)雅的方式來(lái)編寫網(wǎng)絡(luò)層代碼。RxSwift + Moya 是個(gè)不錯(cuò)的選擇雀彼,不僅能使代碼更優(yōu)雅美觀壤蚜,方便維護(hù),還有具有一些很實(shí)用的小功能徊哑。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末袜刷,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子莺丑,更是在濱河造成了極大的恐慌著蟹,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異草则,居然都是意外死亡钢拧,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門炕横,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)源内,“玉大人,你說(shuō)我怎么就攤上這事份殿∧さ觯” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵卿嘲,是天一觀的道長(zhǎng)颂斜。 經(jīng)常有香客問(wèn)我,道長(zhǎng)拾枣,這世上最難降的妖魔是什么沃疮? 我笑而不...
    開(kāi)封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮梅肤,結(jié)果婚禮上司蔬,老公的妹妹穿的比我還像新娘。我一直安慰自己姨蝴,他們只是感情好俊啼,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著左医,像睡著了一般授帕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上浮梢,一...
    開(kāi)封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天跛十,我揣著相機(jī)與錄音,去河邊找鬼黔寇。 笑死偶器,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的缝裤。 我是一名探鬼主播屏轰,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼憋飞!你這毒婦竟也來(lái)了霎苗?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤榛做,失蹤者是張志新(化名)和其女友劉穎唁盏,沒(méi)想到半個(gè)月后内狸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡厘擂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年昆淡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刽严。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡昂灵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出舞萄,到底是詐尸還是另有隱情眨补,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布倒脓,位于F島的核電站撑螺,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏崎弃。R本人自食惡果不足惜甘晤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望饲做。 院中可真熱鬧安皱,春花似錦、人聲如沸艇炎。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)缀踪。三九已至,卻和暖如春虹脯,著一層夾襖步出監(jiān)牢的瞬間驴娃,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工循集, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留唇敞,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓咒彤,卻偏偏與公主長(zhǎng)得像疆柔,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子镶柱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容