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)建用了 SwiftyJSON
和 Moya_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è)接口,willSendRequest
和 didReceiveResponse
狡逢,可以在請(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í)用的小功能徊哑。