https://www.jianshu.com/p/0d0f818be6c4
2017.04.13 15:07* 字數 1574 閱讀 3632評論 16喜歡 27
前言
公司之前的項目是由其他同事搭建的,隨着公司業務的拓展,網絡請求隨之增加。網絡工具類內部的代碼愈發龐大,最終難以管理。為此尋找一個可行的解決方案,順便學習一下RxSwift的使用。不說那么多底層原理,直接咱就說怎么用、怎么寫,通俗易懂。文章附demo源碼,由於本人也在學習中,所以代碼中難免存在疏漏,存在問題可以互相討論。demo內api選自公司內部api,所以不過分使用,僅作實例而已。
博客園demo
https://github.com/sundaysmeSwift/Moyastudy
環境配置
- Xcode 8.3
- Swift 3
cocoapods
- Alamofire
- ObjectMapper
- Moya
- RxSwift
- RxCocoa
- PromiseKit
其中,數據解析部分(ObjectMapper)我們根據各自公司的習慣,可以選擇Argo,SwiftyJSON等等其他框架,這不影響本博客內容。
實例
Moya部分
Moya是一個網絡抽象層庫。它在Alamofire基礎上提供了一系列簡單的抽象接口,讓客戶端代碼不用去直接調用Alamofire,同時提供了很多實用的功能。
創建請求
Moya的TargetType協議規定使用枚舉來創建網絡請求,我們可以通過枚舉來區分不同業務的網絡請求。例如:
enum UserAPI {
case registerUser(name: String, password: String)
case upload(avator: UIImage)
}
enum homepageAPI {
case homepageData
case homepageBannerData
}
這里我們使用demo里ApiExample的實例,分別表示一個get、post和一個上傳圖片的請求,下載的請求一般只有特殊的業務里才會用到,這里不做展示。
enum ApiExample {
case frontpage
case fetchMorePackageArts(count: Int, artStyleId: Int, artType: Int)
case updateExample(image: UIImage, otherParameter: String)
}
定義請求枚舉完成后,我們需要在枚舉的拓展里實現Moya的TargetType協議。
extension ApiExample: TargetType {
public var baseURL: URL {
return URL(string: "http://zuzu.artally.com.cn/zuzuart/")!
}
public var path: String {
switch self {
case .frontpage:
return "frontpage/index/list/"
case .fetchMorePackageArts:
return "work/banner/work/list10/"
case .updateExample:
return "這里不提供實例,使用偽代碼"
}
}
public var method: Moya.Method {
switch self {
case .frontpage:
return .get
case .fetchMorePackageArts, .updateExample:
return .post
}
}
public var parameters: [String: Any]? {
switch self {
case .frontpage:
return nil
case .fetchMorePackageArts(let count, let artStyleId, let artType):
return ["count": count, "banner_style_id": artStyleId, "type": artType]
case .updateExample(_, let otherParameter):
// 這里返回除圖片之外的所有參數
return ["參數名":otherParameter]
}
}
// Local data for unit test.use empty data temporarily.
public var sampleData: Data {
return "".data(using: .utf8)!
}
// Represents an HTTP task.
public var task: Task {
switch self {
case .updateExample(let image, _):
let data = UIImageJPEGRepresentation(image, 0.7)
let img = MultipartFormData(provider: .data(data!), name: "參數名", fileName: "名稱隨便寫.jpg", mimeType: "image/jpeg")
return .upload(.multipart([img]))
default:
return .request
}
}
public var parameterEncoding: ParameterEncoding {
// Select type of parameter encoding based on requirements.Usually we use 'URLEncoding.default'.
/*
if self.method == .get || self.method == .head {
return URLEncoding.default
} else {
return JSONEncoding.default
}
*/
return URLEncoding.default
}
這里對幾個特殊的參數做出解釋。sampleData是單元測試等等需要的假數據,只在有測試的需求下才用得到,一般我們返回一個空的數據就可以。parameterEncoding是參數編碼方式,通常我們使用URLEncoding就可以,根據不同的需求去選擇,如實例中注釋代碼。
插件機制
如果我們想在請求前和請求后做一些操作,Moya提供自定義請求插件來實現這一需求。比如我們要做請求完成前顯示菊花,請求完成或失敗后隱藏菊花,我們可以這樣做。自定義插件需要遵守PluginType協議,根據不同需求實現協議里的方法。實例:
public final class RequestLoadingPlugin: PluginType {
public func willSend(_ request: RequestType, target: TargetType) {
// show loading
}
public func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
// hide loading
}
}
ObjectMapper解析數據
我們使用ObjectMapper解析數據映射為model,ObjectMapper的使用這里不作介紹,請看官方文檔。
我們為RxSwift中Observable寫一個拓展,用於解析數據。通常我們服務器后台返回的數據格式類似以下形式,有的直接是一個我們要用到的json數據,有的在數據外還包一層表示請求狀態的無用數據(code,msg 等等):
// success
{
code: 200,
msg: "success",
work_info: { }
}
or
{
work_comments: [ ]
}
// error
{
code: 500,
error: "token error",
}
所以在拓展里面提供兩個方法去解析模型和數組,並可選的傳入key值來解析返回的json數據中字典對應的key值。
func mapObject<T: Mappable>(type: T.Type, key: String? = nil) -> Observable<T> {
}
func mapArray<T: Mappable>(type: T.Type, key: String? = nil) -> Observable<[T]> {
}
另外我們可以自定義錯誤類型來處理數據解析中發生的錯誤。
enum RxSwiftMoyaError: String {
case ParseJSONError
case OtherError
// you can define other error
}
extension RxSwiftMoyaError: Swift.Error {}
有時候請求是成功的,但是請求內容是錯誤的,錯誤信息由我們的服務器返回,如前文提及。所以我們在這個拓展里提供了一個方法解析服務器返回的錯誤信息,若有錯誤則拋出,無則返回nil
fileprivate func parseError(response: [String: Any]?) -> NSError? {
var error: NSError?
if let value = response {
if let code = value["code"] as? Int, code != 200 {
var msg = ""
if let message = value["error"] as? String {
msg = message
}
error = NSError(domain: "Network", code: code, userInfo: [NSLocalizedDescriptionKey: msg])
}
}
return error
}
異步編程 PromiseKit
現代編程語言都很好的支持了異步編程,因此在swift編程中,擁有功能強大且輕量級的異步編程工具的需求變得很強烈。
這是PromiseKit中提到的,所以我們不甘落后,也想來一發。
如官方文檔中提到的異步鏈式調用,這很常見。詳細使用請看官方文檔,這里不做介紹。
login().then { json in
//
}.catch { error in
// handle error
}
例如,在本demo中我們使用MVVM模式。在viewModel中我們處理網絡請求。在demo中ViewModelExample文件內,我們書寫以下代碼。首先,我們使用Moya的RxSwift拓展,來創建一個RxMoyaProvider,用於請求數據。在這里,我們創建Provider時可以自主選擇插件,用於不同的需求。例如有些網絡請求我們並不希望用戶看到,所以不需要出現加載提示(如菊花,等等),我們在創建時就不需要加入插件。反之,我們可以在創建Provider時加入所需的插件以適應不同的需求。因此Moya變得更加靈活。實例:
let provider = RxMoyaProvider<ApiExample>(plugins: [RequestLoadingPlugin(),NetworkLogger()])
接着我們來創建一個Promise(異步任務)。
func getHomepagePageData() -> Promise<HomepageData> {
return Promise(resolvers: { (result, error) in
provider.request(.frontpage)
.filterSuccessfulStatusCodes()
.mapJSON()
.mapObject(type: HomepageData.self)
.subscribe(onNext: {
result($0)
}, onError: {
error($0)
})
.addDisposableTo(disposeBag)
})
}
在這個方法內部,我們返回一個Promise,在實例化promise內部使用Moya請求數據。然后通過Observable拓展中的* .mapObject或者.mapArray方法來解析數據並返回,其中產生的錯誤通過.onError*回調進行處理。通過RxSwift鏈式調用,我們可以很簡潔的書寫網絡請求的代碼,是不是開始有些感覺了?
provider.request(.frontpage)
.filterSuccessfulStatusCodes()
.mapJSON()
.mapObject(type: HomepageData.self)
.subscribe(onNext: {
result($0)
}, onError: {
error($0)
})
.addDisposableTo(disposeBag)
使用實例
我們在控制器里添加一個viewModel的實例,然后在網絡請求的方法是實現viewModel中定義的相關請求方法即可。實例:
lazy var viewModel = ViewModelExample()
func getRequestExample() {
viewModel.getHomepagePageData().then { data in
// fetch data to refresh UI
print(data.msg ?? "")
}.always {
// optional
// always do something before request complete,such as cache data,etc.
print("request complete")
}.catch { (error) in
// handle error
print(error)
}
}
通過PromiseKit直接鏈式調用,我們很簡潔的通過.then一步步的處理數據,通過.catch處理錯誤。通過Moya和RxSwift,我們發送請求和解析數據也變得靈活機動干凈利落,不拖泥帶水。同時因為我們通過枚舉來管理不同業務的請求接口,代碼邏輯也變得清晰。
總之,Moya+PromiseKit+Swift 所謂優雅的書寫網絡請求,非常值得嘗試一下!
相關參考: