基于MVVM構(gòu)建聊天App (三)網(wǎng)絡(luò)請(qǐng)求封裝

封面

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

Github 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)很大的麻煩;

友情鏈接:

面向協(xié)議編程與 Cocoa 的邂逅

Sample Music list app

Github RxSwift

RxSwift 中文網(wǎng)

泊學(xué)網(wǎng)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容