基于MVVM构建聊天App (三)网络请求封装

封面

Github 基于MVVM构建聊天App (三)网络请求封装

本文主要处理2个问题:

  • 请求Loading扩展处理
  • 封装URLSession返回Observable序列

1、请求Loading扩展处理

关于Loading组件,我已经封装好,并发布在Github上,RPToastView,使用方法可参考README.md
此处只需对UIViewController做一个extension,用一个属性来控制Loading组件的显示和隐藏即可,核心代码如下:

extension Reactive where Base: UIViewController {
    public var isAnimating: Binder {
        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的基础上又封装了一层,但是我在工程中并没有使用Moya,主要是基于以下3点考虑:

  • (1)、Moya自身原因:Moya封装的很完美,这虽然为开发者带来了很大的方便,但是过多封装的必然会导致可扩展性下降
  • (2)、内部原因:由于我公司的后台接口没有一个统一的标准,所以不同模块后台返回的数据结构不同,所以我不得不分开处理
  • (3)、基于App包大小考虑:导入过多的第三方开源库必然会使App包也同步变大,这并不是我所期望的

所以我最终的选择是RxSwift+URLSession+SwiftyJSON

3、RxSwift的使用

关于网络请求,OC中常用的开源库是AFNetworking,在Swift中我们常用Alamofire。截止2020年12月AFNetworking的star数量是33.1K,Alamofire的star数量是35K。从这个数据来说,Swift虽然是一门新的语言,但更受开发者青睐。

网络请求最简单的方法个人觉得用 Alamofire通过Closures返回是否成功或失败:

func post(with body: [String : AnyObject], _ path: String, with closures: @escaping ((_ json: [String : AnyObject],_ failure : String?) -> Void))

如果我们在用户登录成功后需要再调一次接口查询该用户Socket服务器相关数据,那么请求的代码就会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可以将多个请求合并处理,参考RxSwift:等待多个并发任务完成后处理结果

  • 1、更直观简洁的RxSwift

同时,使用RxSwift,返回一个Observable,还可以避免嵌套回调的问题。

上面的代码用RxSwift来写,就更符合逻辑了:

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、处理服务器返回的数据

一般一个请求无非是三种情况:

  • 请求成功时服务器返回的数据结构
  • 请求服务器成功,但返回数据异常,如参数错误,加密处理异常,登录超时等
  • 请求没有成功,根据返回的错误码做处理

创建一个协议来管理请求,此处需要知道请求的API,HTTP方式,所需参数等,代码如下:

/// 请求服务器相关
public protocol Request {
    var path: String {get}
    var method: HTTPMethod {get}
    var parameter: [String: AnyObject]? {get}
    var host: String {get}
}

在发起一个请求时可能不需要任何参数,此处做一个extension处理将parameter作为可选参数即可:

extension Request {
    var parameter: [String: AnyObject] {
        return [:]
    }
}

此处要分别对以上三种情况做出处理,首先来看看服务器给的接口文档,请求成功时服务器返回的数据结构:

{
  "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"
}

请求成功,但出现异常时返回的数据结构:

{
  "returnCode" : "601",
  "returnMsg" : "登录失效",
}

新建一个SignInModel.Swift来作为模型

public struct SignInModel {
    public let username,dept_id,access_token,token_type,user_id,scope,refresh_token,expires_in,license: String   
}

将返回的SwiftyJSON对象转为Model对象

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
    }
}

当请求成功后,将服务器获取的Data数据转成SwiftyJSON实例,然后在ViewModel中转成SignInModel。

对于请求成功时,但返回数据异常时,可根据后台返回的code码和message信息,给用户一个友好提示。

对于请求服务器失败时情况,可以定义一个enum来处理:

/// 请求服务器失败时 错误码
public enum RequestError: Error {
   case unknownError
   case connectionError
   case timeoutError
   case authorizationError(JSON)
   case notFound
   case serverError
}

4、发起请求并返回一个Observable对象

RxSwift对系统提供的URLSession也做了扩展,可以让开发者直接使用:

URLSession.shared.rx.response(request: urlRequest).subscribe(onNext: { (response, data) in
            
}).disposed(by: disposeBag)

首先定一个可以发送请求的协议, 无论请求成功还是失败都需要返回一个Observable队列,此处使用了一个泛型,任何一个遵循AuthRemoteProtocol的类型都可以实现网络请求。

public protocol AuthRemoteProtocol {
  func post(_ r: T) -> Observable
}

当发起一个请求时,我们需要对URLSession做一些请求配置,如设置header、body、url、timeout、请求方式等,才能顺利的完成一个请求。header、timeout这几个参数一般都固定的。而body、url这两个参数必须是一个遵循Request协议的对象。核心代码如下:

public func post(_ r: T) -> Observable {
    // 设置请求API
    guard let path = URL(string: r.host.appending(r.path)) else {         
        return .error(RequestError.unknownError)
    }
    var headers: [String : String]?
    // 设置超时时间
    var urlRequest = URLRequest(url: path, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 30)
    // 设置header
    urlRequest.allHTTPHeaderFields = headers
    // 设置请求方式
    urlRequest.httpMethod = r.method.rawValue
    return Observable.create { (observer) -> Disposable in
       URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
          // 根据服务器返回的code处理并传递给ViewModel 
       }.resume()
       return Disposables.create { }
    }
}

一般跟服务器约定,当服务器返回的code为200时我们认为服务器请求成功并正常返回数据,当返回其他code
时根据返回的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 {
    /// 协议方式,成功返回JSON -----> RxSwift
    func requestData(_ r: T) -> Observable
}

public struct RPAuthRemoteAPI: AuthRemoteProtocol {
    /// 协议方式,成功返回JSON -----> RxSwift
    public func post(_ r: T) -> Observable {
        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中调用,并根据服务器返回的code做处理:

// 显示LoadingView
self.loading.onNext(true)
RPAuthRemoteAPI().post(SigninRequest(parameter: [:], path: path))
 .subscribe(onNext: { returnJson in
  // JSON对象转成Model,同时本地缓存Token
    self.loading.onNext(true)
 }, onError: { errorJson in
  // 失败
  self.loading.onNext(true)
}, onCompleted: {
  // 调用完成时
}).disposed(by: disposeBag)

5、存在问题

虽然以上的方法基于POP的实现,利于代码的扩展和维护。但是我觉得也存在问题:

  • 过分依赖RxSwift、SwiftyJSON第三方库,如果说出现系统版本升级,或者这些第三方库的作者不再维护等问题,会给我们后期的开发和维护带来很大的麻烦;

友情链接:

面向协议编程与 Cocoa 的邂逅

Sample Music list app

Github RxSwift

RxSwift 中文网

泊学网

你可能感兴趣的:(基于MVVM构建聊天App (三)网络请求封装)