作者:@請叫我汪二 授權本站轉載。
最近在新項目中嘗試使用 Moya+RxSwift+Argo 進行網絡請求和解析,感覺還闊以,再來給大家安利一波。
Moya
是一個基於 Alamofire
的更高層網絡請求封裝,深入學習請參見官方文檔:Moya/Docs。
使用 Moya
之後網絡請求一般長了這樣:
provider.request(.UserProfile("ashfurrow")) { (data, statusCode, response, error) in if let data = data { // do something with the data } }
Moya
提供了很多不錯的特性,其中我感覺最棒的是 stub
,配合 sampleData
分分鐘就完成了單元測試:
private let provider = MoyaProvider(stubClosure: MoyaProvider.ImmediatelyStub)
注意這裡的 MoyaProvider.ImmediatelyStub
,我原以為它是個枚舉類型,看了 MoyaProvider
定義發現這裡應該傳個 closure
,看了 ImmediatelyStub
的定義發現原來它是個類方法:
public typealias StubClosure = Target -> Moya.StubBehavior override public init(stubClosure: StubClosure = MoyaProvider.NeverStub, ...) { } public final class func ImmediatelyStub(_: Target) -> Moya.StubBehavior { return .Immediate }
如果想打印每次請求的參數,在組裝 endpoint
的時候打印即可:
private func endpointMapping(target: Target) -> Endpoint { if let parameters = target.parameters { log.verbose("\(parameters)") } return MoyaProvider.DefaultEndpointMapping(target) } private let provider = RxMoyaProvider(endpointClosure: endpointMapping)
RxSwift
前面強行安利過兩波,在此不再贅述啦,Moya
本身提供了 RxSwift
擴展,可以無縫銜接 RxSwift
和 ReactiveCocoa
,於是打開方式變成了這樣:
private let provider = RxMoyaProvider() private var disposeBag = DisposeBag() extension ItemAPI { static func getNewItems(completion: [Item] -> Void) { disposeBag = DisposeBag() provider .request(.GetItems()) .subscribe( onNext: { items in completion(items) } ) .addDisposableTo(disposeBag) } }
Moya
的核心開發者、同時也是 Artsy 的成員:Ash Furrow, 在 AltConf 做過一次 《Functional Reactive Awesomeness With Swift》 的分享,推薦大家看一下,很可愛的!
Argo
是 thoughtbot
開源的函數式 JSON
解析轉換庫。說到 thoughtbot
就不得不提他司關於 JSON
解析質量很高的一系列文章:
Efficient JSON in Swift with Functional Concepts and Generics
Real World JSON Parsing with Swift
Parsing Embedded JSON and Arrays in Swift
Functional Swift for Dealing with Optional Values
Argo
基本上就是沿著這些文章的思路寫出來的,相關的庫還有 Runes 和 Curry。
使用 Argo
做 JSON
解析很有意思,大致長這樣:
struct Item { let id: String let url: String } extension Item: Decodable { static func decode(j: JSON) -> Decoded { return curry(Item.init) j <| "id" j <| "url" } }
至於這其中各種符號的緣由,在幾篇博客中都有講解,還是挺有意思滴。
說完這三者,如何把它們串起來呢?Emergence 中的 Observable/Networking 給了我們答案。稍微整理後如下:
enum ORMError : ErrorType { case ORMNoRepresentor case ORMNotSuccessfulHTTP case ORMNoData case ORMCouldNotMakeObjectError } extension Observable { private func resultFromJSON(object:[String: AnyObject], classType: T.Type) -> T? { let decoded = classType.decode(JSON.parse(object)) switch decoded { case .Success(let result): return result as? T case .Failure(let error): log.error("\(error)") return nil } } func mapSuccessfulHTTPToObject(type: T.Type) -> Observable { return map { representor in guard let response = representor as? MoyaResponse else { throw ORMError.ORMNoRepresentor } guard ((200...209) ~= response.statusCode) else { if let json = try? NSJSONSerialization.JSONObjectWithData(response.data, options: .AllowFragments) as? [String: AnyObject] { log.error("Got error message: \(json)") } throw ORMError.ORMNotSuccessfulHTTP } do { guard let json = try NSJSONSerialization.JSONObjectWithData(response.data, options: .AllowFragments) as? [String: AnyObject] else { throw ORMError.ORMCouldNotMakeObjectError } return self.resultFromJSON(json, classType:type)! } catch { throw ORMError.ORMCouldNotMakeObjectError } } } func mapSuccessfulHTTPToObjectArray(type: T.Type) -> Observable { return map { response in guard let response = response as? MoyaResponse else { throw ORMError.ORMNoRepresentor } // Allow successful HTTP codes guard ((200...209) ~= response.statusCode) else { if let json = try? NSJSONSerialization.JSONObjectWithData(response.data, options: .AllowFragments) as? [String: AnyObject] { log.error("Got error message: \(json)") } throw ORMError.ORMNotSuccessfulHTTP } do { guard let json = try NSJSONSerialization.JSONObjectWithData(response.data, options: .AllowFragments) as? [[String : AnyObject]] else { throw ORMError.ORMCouldNotMakeObjectError } // Objects are not guaranteed, thus cannot directly map. var objects = [T]() for dict in json { if let obj = self.resultFromJSON(dict, classType:type) { objects.append(obj) } } return objects } catch { throw ORMError.ORMCouldNotMakeObjectError } } } }
這樣在調用的時候就很舒服了,以前面的 Item
為例:
private let provider = RxMoyaProvider() private var disposeBag = DisposeBag() extension ItemAPI { static func getNewItems(records:[Record] = [], needCount: Int, completion: [Item] -> Void) { disposeBag = DisposeBag() provider .request(.AddRecords(records, needCount)) .mapSuccessfulHTTPToObjectArray(Item) .subscribe( onNext: { items in completion(items) } ) .addDisposableTo(disposeBag) } }
一個 mapSuccessfulHTTPToObjectArray
方法,直接將 JSON
字符串轉換成了 Item
對象,並且傳入了後面的數據流中,所以在 onNext
訂閱的時候傳入的就是 [Item]
數據,並且這個轉換過程還是可以復用的,且適用於所有網絡請求中 JSON
和 Model
的轉換。爽就一個字,我只說一次。
爽!
匆匆讀了一點 Emergence 和 Eidolon 的項目源碼,沒有深入不過已經受益匪淺。通過 bundle 管理 id 和 key 直接解決了我當初糾結已久的『完整項目開源如何優雅地保留 git 記錄且保護項目隱私』的問題,還有 Moya/RxSwift
和 Moya/ReactiveCocoa
這種子模塊化處理也在共有模塊管理這個問題上給了我一些啟發。
真是很喜歡 Artsy 這樣的團隊,大家都一起做著自己喜歡的事情,還能站著把錢賺了。
所幸的是我也可以這樣做自己喜歡的事情了,不過不賺錢。具體狀況後面單獨開一篇閒扯扯。
碎告。
參考資料:
RxSwift
Moya
Argo
Emergence
Eidolon
Efficient JSON in Swift with Functional Concepts and Generics
Real World JSON Parsing with Swift
Parsing Embedded JSON and Arrays in Swift
Functional Swift for Dealing with Optional Values