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

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

作者: 小時間光 | 来源:发表于2021-01-28 17:54 被阅读0次
    封面

    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<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的基础上又封装了一层,但是我在工程中并没有使用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队列,此处使用了一个<T: Request>泛型,任何一个遵循AuthRemoteProtocol的类型都可以实现网络请求。

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

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

    public func post<T: Request>(_ r: T) -> Observable<JSON> {
        // 设置请求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<T: Request>(_ r: T) -> Observable<JSON>
    }
    
    public struct RPAuthRemoteAPI: AuthRemoteProtocol {
        /// 协议方式,成功返回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中调用,并根据服务器返回的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 (三)网络请求封装

          本文链接:https://www.haomeiwen.com/subject/qrjxtltx.html