
Github 基于MVVM構(gòu)建聊天App (三)網(wǎng)絡(luò)請(qǐng)求封裝
本文主要處理2個(gè)問(wèn)題:
- 請(qǐng)求Loading擴(kuò)展處理
- 封裝URLSession返回Observable序列
1、請(qǐng)求Loading擴(kuò)展處理
關(guān)于Loading組件,我已經(jīng)封裝好,并發(fā)布在Github上,RPToastView,使用方法可參考README.md。
此處只需對(duì)UIViewController做一個(gè)extension,用一個(gè)屬性來(lái)控制Loading組件的顯示和隱藏即可,核心代碼如下:
extension Reactive where Base: UIViewController {
public var isAnimating: Binder<Bool> {
return Binder(self.base, binding: { (vc, active) in
if active == true {
// 顯示Loading View
} else {
// 隱藏Loading View
}
})
}
}
此處給isAnimating傳入true表示顯示LoadingView,傳入false表示隱藏LoadingView,
2、為什么不使用Moya
Moya是在常用的Alamofire的基礎(chǔ)上又封裝了一層,但是我在工程中并沒(méi)有使用Moya,主要是基于以下3點(diǎn)考慮:
- (1)、Moya自身原因:Moya封裝的很完美,這雖然為開發(fā)者帶來(lái)了很大的方便,但是過(guò)多封裝的必然會(huì)導(dǎo)致可擴(kuò)展性下降
- (2)、內(nèi)部原因:由于我公司的后臺(tái)接口沒(méi)有一個(gè)統(tǒng)一的標(biāo)準(zhǔn),所以不同模塊后臺(tái)返回的數(shù)據(jù)結(jié)構(gòu)不同,所以我不得不分開處理
- (3)、基于App包大小考慮:導(dǎo)入過(guò)多的第三方開源庫(kù)必然會(huì)使App包也同步變大,這并不是我所期望的
所以我最終的選擇是RxSwift+URLSession+SwiftyJSON。
3、RxSwift的使用
關(guān)于網(wǎng)絡(luò)請(qǐng)求,OC中常用的開源庫(kù)是AFNetworking,在Swift中我們常用Alamofire。截止2020年12月AFNetworking的star數(shù)量是33.1K,Alamofire的star數(shù)量是35K。從這個(gè)數(shù)據(jù)來(lái)說(shuō),Swift雖然是一門新的語(yǔ)言,但更受開發(fā)者青睞。
網(wǎng)絡(luò)請(qǐng)求最簡(jiǎn)單的方法個(gè)人覺(jué)得用 Alamofire通過(guò)Closures返回是否成功或失敗:
func post(with body: [String : AnyObject], _ path: String, with closures: @escaping ((_ json: [String : AnyObject],_ failure : String?) -> Void))
如果我們?cè)谟脩舻卿洺晒笮枰僬{(diào)一次接口查詢?cè)撚脩?strong>Socket服務(wù)器相關(guān)數(shù)據(jù),那么請(qǐng)求的代碼就會(huì)Closures里嵌套Closures,
RPAuthRemoteAPI().signIn(with: ["username":"","password":""], signInAPI) { (siginInfo, errorMsg) in
if let errorMsg = errorMsg {
} else {
RPAuthRemoteAPI().socketInfo(with: ["username":""], userInfoAPI) { (userInfo, userInfoErrorMsg) in
if let userInfoErrorMsg = userInfoErrorMsg {
} else {
}
}
}
}
使用RxSwift可以將多個(gè)請(qǐng)求合并處理,參考RxSwift:等待多個(gè)并發(fā)任務(wù)完成后處理結(jié)果
- 1、更直觀簡(jiǎn)潔的RxSwift
同時(shí),使用RxSwift,返回一個(gè)Observable,還可以避免嵌套回調(diào)的問(wèn)題。
上面的代碼用RxSwift來(lái)寫,就更符合邏輯了:
let _ = RPAuthRemoteAPI().signIn(with: ["username":"","password":""], signInAPI)
.flatMap({ (returnJson) in
return RPAuthRemoteAPI().userInfo(with: ["username":""], userInfoAPI)
}).subscribe { (json) in
print("用戶信息-----------: \(json)")
} onError: { (error) in
} onCompleted: {
} onDisposed: {
}
- 2、處理服務(wù)器返回的數(shù)據(jù)
一般一個(gè)請(qǐng)求無(wú)非是三種情況:
- 請(qǐng)求成功時(shí)服務(wù)器返回的數(shù)據(jù)結(jié)構(gòu)
- 請(qǐng)求服務(wù)器成功,但返回?cái)?shù)據(jù)異常,如參數(shù)錯(cuò)誤,加密處理異常,登錄超時(shí)等
- 請(qǐng)求沒(méi)有成功,根據(jù)返回的錯(cuò)誤碼做處理
創(chuàng)建一個(gè)協(xié)議來(lái)管理請(qǐng)求,此處需要知道請(qǐng)求的API,HTTP方式,所需參數(shù)等,代碼如下:
/// 請(qǐng)求服務(wù)器相關(guān)
public protocol Request {
var path: String {get}
var method: HTTPMethod {get}
var parameter: [String: AnyObject]? {get}
var host: String {get}
}
在發(fā)起一個(gè)請(qǐng)求時(shí)可能不需要任何參數(shù),此處做一個(gè)extension處理將parameter作為可選參數(shù)即可:
extension Request {
var parameter: [String: AnyObject] {
return [:]
}
}
此處要分別對(duì)以上三種情況做出處理,首先來(lái)看看服務(wù)器給的接口文檔,請(qǐng)求成功時(shí)服務(wù)器返回的數(shù)據(jù)結(jié)構(gòu):
{
"access_token" : "b6298027-a985-441c-a36c-d0a362520896",
"user_id" : "1268805326995996673",
"dept_id" : 1,
"license" : "made by tsn",
"scope" : "server",
"token_type" : "bearer",
"username" : "198031",
"expires_in" : 19432,
"refresh_token" : "692a1b6e-051f-424d-bd2e-3a9ccec8d4f2"
}
請(qǐng)求成功,但出現(xiàn)異常時(shí)返回的數(shù)據(jù)結(jié)構(gòu):
{
"returnCode" : "601",
"returnMsg" : "登錄失效",
}
新建一個(gè)SignInModel.Swift來(lái)作為模型
public struct SignInModel {
public let username,dept_id,access_token,token_type,user_id,scope,refresh_token,expires_in,license: String
}
將返回的SwiftyJSON對(duì)象轉(zhuǎn)為Model對(duì)象
extension SignInModel {
public init?(json: JSON) {
username = json["username"].stringValue
dept_id = json["dept_id"].stringValue
access_token = json["access_token"].stringValue
token_type = json["token_type"].stringValue
user_id = json["user_id"].stringValue
scope = json["scope"].stringValue
refresh_token = json["refresh_token"].stringValue
expires_in = json["expires_in"].stringValue
license = json["license"].stringValue
}
}
當(dāng)請(qǐng)求成功后,將服務(wù)器獲取的Data數(shù)據(jù)轉(zhuǎn)成SwiftyJSON實(shí)例,然后在ViewModel中轉(zhuǎn)成SignInModel。
對(duì)于請(qǐng)求成功時(shí),但返回?cái)?shù)據(jù)異常時(shí),可根據(jù)后臺(tái)返回的code碼和message信息,給用戶一個(gè)友好提示。
對(duì)于請(qǐng)求服務(wù)器失敗時(shí)情況,可以定義一個(gè)enum來(lái)處理:
/// 請(qǐng)求服務(wù)器失敗時(shí) 錯(cuò)誤碼
public enum RequestError: Error {
case unknownError
case connectionError
case timeoutError
case authorizationError(JSON)
case notFound
case serverError
}
4、發(fā)起請(qǐng)求并返回一個(gè)Observable對(duì)象
RxSwift對(duì)系統(tǒng)提供的URLSession也做了擴(kuò)展,可以讓開發(fā)者直接使用:
URLSession.shared.rx.response(request: urlRequest).subscribe(onNext: { (response, data) in
}).disposed(by: disposeBag)
首先定一個(gè)可以發(fā)送請(qǐng)求的協(xié)議, 無(wú)論請(qǐng)求成功還是失敗都需要返回一個(gè)Observable隊(duì)列,此處使用了一個(gè)<T: Request>泛型,任何一個(gè)遵循AuthRemoteProtocol的類型都可以實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求。
public protocol AuthRemoteProtocol {
func post<T: Request>(_ r: T) -> Observable<JSON>
}
當(dāng)發(fā)起一個(gè)請(qǐng)求時(shí),我們需要對(duì)URLSession做一些請(qǐng)求配置,如設(shè)置header、body、url、timeout、請(qǐng)求方式等,才能順利的完成一個(gè)請(qǐng)求。header、timeout這幾個(gè)參數(shù)一般都固定的。而body、url這兩個(gè)參數(shù)必須是一個(gè)遵循Request協(xié)議的對(duì)象。核心代碼如下:
public func post<T: Request>(_ r: T) -> Observable<JSON> {
// 設(shè)置請(qǐng)求API
guard let path = URL(string: r.host.appending(r.path)) else {
return .error(RequestError.unknownError)
}
var headers: [String : String]?
// 設(shè)置超時(shí)時(shí)間
var urlRequest = URLRequest(url: path, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 30)
// 設(shè)置header
urlRequest.allHTTPHeaderFields = headers
// 設(shè)置請(qǐng)求方式
urlRequest.httpMethod = r.method.rawValue
return Observable.create { (observer) -> Disposable in
URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
// 根據(jù)服務(wù)器返回的code處理并傳遞給ViewModel
}.resume()
return Disposables.create { }
}
}
一般跟服務(wù)器約定,當(dāng)服務(wù)器返回的code為200時(shí)我們認(rèn)為服務(wù)器請(qǐng)求成功并正常返回?cái)?shù)據(jù),當(dāng)返回其他code
時(shí)根據(jù)返回的code做出處理。最終的代碼如下:
/// 登錄Request
struct SigninRequest: Request {
typealias Response = SigninRequest
var parameter: [String : AnyObject]?
var path: String
var method: HTTPMethod = .post
var host: String {
return __serverTestURL
}
}
public enum RequestError: Error {
case unknownError
case connectionError
case timeoutError
case authorizationError(JSON)
case notFound
case serverError
}
public protocol AuthRemoteProtocol {
/// 協(xié)議方式,成功返回JSON -----> RxSwift
func requestData<T: Request>(_ r: T) -> Observable<JSON>
}
public struct RPAuthRemoteAPI: AuthRemoteProtocol {
/// 協(xié)議方式,成功返回JSON -----> RxSwift
public func post<T: Request>(_ r: T) -> Observable<JSON> {
let path = URL(string: r.host.appending(r.path))!
var urlRequest = URLRequest(url: path, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 30)
urlRequest.allHTTPHeaderFields = ["Content-Type" : "application/x-www-form-urlencoded; application/json; charset=utf-8;"]
urlRequest.httpMethod = r.method.rawValue
if let parameter = r.parameter {
// --> Data
let parameterData = parameter.reduce("") { (result, param) -> String in
return result + "&\(param.key)=\(param.value as! String)"
}.data(using: .utf8)
urlRequest.httpBody = parameterData
}
return Observable.create { (observer) -> Disposable in
URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if let error = error {
print(error)
observer.onError(RequestError.connectionError)
} else if let data = data ,let responseCode = response as? HTTPURLResponse {
do {
let json = try JSON(data: data)
switch responseCode.statusCode {
case 200:
print("json-------------\(json)")
observer.onNext(json)
observer.onCompleted()
break
case 201...299:
observer.onError(RequestError.authorizationError(json))
break
case 400...499:
observer.onError(RequestError.authorizationError(json))
break
case 500...599:
observer.onError(RequestError.serverError)
break
case 600...699:
observer.onError(RequestError.authorizationError(json))
break
default:
observer.onError(RequestError.unknownError)
break
}
}
catch let parseJSONError {
observer.onError(parseJSONError)
print("error on parsing request to JSON : \(parseJSONError)")
}
}
}.resume()
return Disposables.create { }
}
}
在ViewModel中調(diào)用,并根據(jù)服務(wù)器返回的code做處理:
// 顯示LoadingView
self.loading.onNext(true)
RPAuthRemoteAPI().post(SigninRequest(parameter: [:], path: path))
.subscribe(onNext: { returnJson in
// JSON對(duì)象轉(zhuǎn)成Model,同時(shí)本地緩存Token
self.loading.onNext(true)
}, onError: { errorJson in
// 失敗
self.loading.onNext(true)
}, onCompleted: {
// 調(diào)用完成時(shí)
}).disposed(by: disposeBag)
5、存在問(wèn)題
雖然以上的方法基于POP的實(shí)現(xiàn),利于代碼的擴(kuò)展和維護(hù)。但是我覺(jué)得也存在問(wèn)題:
- 過(guò)分依賴RxSwift、SwiftyJSON第三方庫(kù),如果說(shuō)出現(xiàn)系統(tǒng)版本升級(jí),或者這些第三方庫(kù)的作者不再維護(hù)等問(wèn)題,會(huì)給我們后期的開發(fā)和維護(hù)帶來(lái)很大的麻煩;
友情鏈接: