
作者:@請叫我汪二 授權本站轉載。
最近在新項目中嘗試使用 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