通過 Moya+RxSwift+Argo 完成網絡請求

jopen 9年前發布 | 26K 次閱讀 Moya iOS開發 移動開發

最近在新項目中嘗試使用 Moya+RxSwift+Argo 進行網絡請求和解析,感覺還闊以,再來給大家安利一波。

Moya

Moya是一個基于Alamofire的更高層網絡請求封裝,深入學習請參見官方文檔:Moya/Docs

使用Moya之后網絡請求一般長了這樣:

</span>
provider.request(.UserProfile("ashfurrow")) { (data, statusCode, response, error) in
    if let data = data {
        // do something with the data
    }
}

Moya提供了很多不錯的特性,其中我感覺最棒的是stub,配合sampleData分分鐘就完成了單元測試:

</tr> </tbody> </table>

注意這里的MoyaProvider.ImmediatelyStub,我原以為它是個枚舉類型,看了MoyaProvider定義發現這里應該傳個closure,看了ImmediatelyStub的定義發現原來它是個類方法:

</span>
public typealias StubClosure = Target -> Moya.StubBehavior

override public init(stubClosure: StubClosure = MoyaProvider.NeverStub, ...) {

}

public final class func ImmediatelyStub(_: Target) -> Moya.StubBehavior { return .Immediate }</pre>
如果想打印每次請求的參數,在組裝endpoint的時候打印即可:

private func endpointMapping<Target: MoyaTarget>(target: Target) -> Endpoint<Target> {
    if let parameters = target.parameters {
        log.verbose("(parameters)")
    }
    return MoyaProvider.DefaultEndpointMapping(target)
}

private let provider = RxMoyaProvider<ItemAPI>(endpointClosure: endpointMapping)</pre>

RxSwift

RxSwift前面強行安利過兩波,在此不再贅述啦,Moya本身提供了RxSwift擴展,可以無縫銜接RxSwift和ReactiveCocoa,于是打開方式變成了這樣:

</span>
private let provider = RxMoyaProvider<ItemAPI>()
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) } }</pre>

Moya的核心開發者、同時也是 Artsy 的成員:Ash Furrow, 在 AltConf 做過一次 《Functional Reactive Awesomeness With Swift》 的分享,推薦大家看一下,很可愛的!

Argo

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
  • </ul>

    Argo基本上就是沿著這些文章的思路寫出來的,相關的庫還有 RunesCurry

    使用Argo做JSON解析很有意思,大致長這樣:

    </span>
    struct Item {
        let id: String
        let url: String
    }

    extension Item: Decodable { static func decode(j: JSON) -> Decoded<Item> { return curry(Item.init) <^> j <| "id" <*> j <| "url" } }</pre>

    至于這其中各種符號的緣由,在幾篇博客中都有講解,還是挺有意思滴。

    All

    說完這三者,如何把它們串起來呢?Emergence 中的 Observable/Networking 給了我們答案。稍微整理后如下:

    </span>
    enum ORMError : ErrorType {
        case ORMNoRepresentor
        case ORMNotSuccessfulHTTP
        case ORMNoData
        case ORMCouldNotMakeObjectError
    }

    extension Observable { private func resultFromJSON<T: Decodable>(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<T: Decodable>(type: T.Type) -> Observable<T> {
        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<T: Decodable>(type: T.Type) -> Observable<[T]> {
        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
            }
        }
    }
    

    }</pre>

    這樣在調用的時候就很舒服了,以前面的Item為例:

    private let provider = RxMoyaProvider<ItemAPI>()
    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) } }</pre>

    一個mapSuccessfulHTTPToObjectArray方法,直接將JSON字符串轉換成了Item對象,并且傳入了后面的數據流中,所以在onNext訂閱的時候傳入的就是[Item]數據,并且這個轉換過程還是可以復用的,且適用于所有網絡請求中JSON和Model的轉換。爽就一個字,我只說一次。

    爽!

    Next

    匆匆讀了一點 EmergenceEidolon 的項目源碼,沒有深入不過已經受益匪淺。通過 bundle 管理 id 和 key 直接解決了我當初糾結已久的『完整項目開源如何優雅地保留 git 記錄且保護項目隱私』的問題,還有Moya/RxSwift和Moya/ReactiveCocoa這種子模塊化處理也在共有模塊管理這個問題上給了我一些啟發。

    真是很喜歡 Artsy 這樣的團隊,大家都一起做著自己喜歡的事情,還能站著把錢賺了。

    所幸的是我也可以這樣做自己喜歡的事情了,不過不賺錢。具體狀況后面單獨開一篇閑扯扯。

    碎告。


    參考資料:

private let provider = MoyaProvider<ItemAPI>(stubClosure: MoyaProvider.ImmediatelyStub) 
  • sesese色