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

RxSwift

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

舉個例子,有如下操作:
點擊按鈕 -> 發(fā)送網(wǎng)絡(luò)請求 -> 對返回的數(shù)據(jù)進行某種格式處理 -> 顯示在一個 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)雅呢?

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

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

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

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

Moya

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

下面進入正題:

創(chuàng)建一個請求

Moya 的 TargetType 協(xié)議規(guī)定的創(chuàng)建網(wǎng)絡(luò)請求的方法,用枚舉來創(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 對象。

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

發(fā)送請求

我們可使用 Moya 自帶一個 RxSwift 的擴展來發(fā)送請求。

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

然后在 ViewController 中就可以寫上面說到過的那一段了

sendRequestButton
    .rx_tap // 觀察按鈕點擊信號
    .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 釋放時,這個綁定關(guān)系也會被釋放

這樣就實現(xiàn)了 點擊按鈕 -> 發(fā)送網(wǎng)絡(luò)請求 -> 顯示結(jié)果
上面這一段沒有考慮錯誤處理,這個后面會說。

URL 緩存

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

離線緩存

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

由于 Moya 沒有提供離線緩存這個功能,只能自己寫了。
為 RxMoyaProvider 擴展離線緩存功能:

extension RxMoyaProvider {
    func tryUseOfflineCacheThenRequest(token: Target) -> Observable<Moya.Response> {
        return Observable.create { [weak self] observer -> Disposable in
            let key = token.cacheKey // 緩存 Key,可以根據(jù)自己的需求來寫,這里采用的是 BaseURL + Path + Parameter轉(zhuǎn)化為JSON字符串
            
            // 先讀取緩存內(nèi)容,有則發(fā)出一個信號(onNext),沒有則跳過
            if let response = HSURLCache.sharedInstance.cachedResponseForKey(key) {
                observer.onNext(response)
            }
            
            // 發(fā)出真正的網(wǎng)絡(luò)請求
            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)建了一個信號序列,當(dāng)有離線緩存時,會發(fā)出一個信號,當(dāng)網(wǎng)絡(luò)請求結(jié)果返回時,會發(fā)出一個信號,當(dāng)網(wǎng)絡(luò)請求失敗時,也會發(fā)出一個錯誤信號。

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

使用離線緩存的網(wǎng)絡(luò)請求方式可以寫成這樣,調(diào)用了上面所說的 tryUseOfflineCacheThenRequest 方法。
并且這里用了 RxSwift 的 distinctUntilChanged 方法,當(dāng)兩個信號完全一樣時,會過濾掉后面的信號。這樣避免頁面在數(shù)據(jù)相同的情況下渲染兩次。

錯誤處理

可以通過判斷 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)

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

這時 Moya 的一個功能,可以在本地放置一個 json 文件,網(wǎng)絡(luò)請求可以設(shè)置成讀取本地文件內(nèi)容來返回數(shù)據(jù)??梢栽诮涌诠收匣驗殚_發(fā)完時,客戶端可以先用假數(shù)據(jù)來開發(fā),先走通流程。

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

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

RxMoyaProvider<DataAPI>(stubClosure: MoyaProvider.ImmediatelyStub)

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

RxMoyaProvider<DataAPI>(stubClosure: MoyaProvider.NeverStub)

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

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

Header 處理

例如如果想要在 Header 中添加一些字段,例如 access-token,可以通過 Moya 的 Endpoint Closure 方式實現(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
    }
}

插件機制

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

Moya 本身提供了打印網(wǎng)路請求日志的插件和 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
        }
    }
}

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

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

結(jié)語

對于用 Swift 編寫的項目來說,可以有比 Objective-C 更優(yōu)雅的方式來編寫網(wǎng)絡(luò)層代碼。RxSwift + Moya 是個不錯的選擇,不僅能使代碼更優(yōu)雅美觀,方便維護,還有具有一些很實用的小功能。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容