美文网首页iOS程序员的业余沙龙iOS-swiftios开发学习
Moya+PromiseKit+RxSwift优雅的书写网络请求

Moya+PromiseKit+RxSwift优雅的书写网络请求

作者: 范范饭特稀 | 来源:发表于2017-04-13 15:07 被阅读2503次

    前言

    公司之前的项目是由其他同事搭建的,随着公司业务的拓展,网络请求随之增加。网络工具类内部的代码愈发庞大,最终难以管理。为此寻找一个可行的解决方案,顺便学习一下RxSwift的使用。不说那么多底层原理,直接咱就说怎么用、怎么写,通俗易懂。文章附demo源码,由于本人也在学习中,所以代码中难免存在疏漏,存在问题可以互相讨论。demo内api选自公司内部api,所以不过分使用,仅作实例而已。

    环境配置

    • 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 所谓优雅的书写网络请求,非常值得尝试一下!

    相关参考:
    学习 Swift Moya(二)- Moya + SwiftyJSON + RxSwift
    如何写出最简洁优雅的网络封装 Moya + RxSwift

    相关文章

      网友评论

      • 易咨酷:最简单的写法能不能提供一下?
        范范饭特稀:@易咨酷 最简单的话,就是把PromiseKit去掉,再去掉解析JSON映射模型的部分, 这已经是最简单的了,稍微看下代码一下就明白的.不明白的话我抽空再更新一下demo吧
      • 90e35846f83b:接口少可以这么写,接口多了呢?有200个接口还要写200个case?还有就是怎么同意上传参数和返回数据的统一处理?
        范范饭特稀:你所谓的参数处理我不太明白,一般传参数需要处理什么? 你用AFN或者Alamofire时还处理参数了?涉及到加密之类的了?如果是硬是要对参数的编码或者请求头之类的东西做处理,那是Alamofire的东西,你去看看Alamofire的文档就是了,返回数据的处理,文章里不都说了么,处理json映射model,包括错误的处理,不都写了么?简直不懂你哪里有疑问. 关于那200个接口写200个case的问题,很多人提过,但是这就是moya的机制,人家就这么用的,你还能咋啊?如果项目里真有200个接口,你用传统的写法不也得写200个接口么,您难道还嫌麻烦不写了不成?so,我只是把自己接触到的东西分享一下而已
        范范饭特稀:...你用传统的方法,200个接口就不用写200个么? 怎么同意? 统一? 什么是上传参数和返回数据的统一处理....:flushed:
      • liubaoyua:PromiseKit 里面的操作也可以用 RxSwift 操作符代替吧
        范范饭特稀:讲真....当初我只是觉得这样好玩,才用了PromiseKit和Rx,一般单独只用一个就可以... PromiseKit不具备Rx"流"的特性,闭包里的东西只会触发一次的.例如这个:.subscribe(onNext: {
        result($0)
        }, onError: {
        error($0)
        }), rx收到信号后会触发成功或者失败的回调,但是里面result($0)这个触发过一次后就不再触发了,这个靛青大神的博客里有讲到.
      • d89590fb8f50:在viewController里,then里面加代码会报错
        func getRequestExample() {
        viewModel.getHomepagePageData().then { data in
        // fetch data to refresh UI
        print("data.msg==\(data)")
        self.view.backgroundColor = UIColor.green
        }.always {
        // optional
        // always do something before request complete,such as cache data,etc.
        print("request complete")
        }.catch { (error) in
        // handle error
        print(error)
        }
        }
        missing return in a closure to return anypromise
        范范饭特稀:我不知道这样写你能否明白,PromiseKit是异步编程的库, firstly -> then -> then等等等-> always-> catch ,这里面每一步都要返回一个Promise供下一步使用,如果下一步用不到,就返回一个空(Void). 这样做的好处很多,比如: firstly {
        // 这里是伪代码,即生成一个Promise,这个promise会提供一个class1,给下一步,当你.then的时候,回车,出现的闭包里面会提供一个class1类型的参数给下一步(then)使用
        // 至于此处提供promise的代码,其实就是我们viewModel中的所有方法,你没发现每个方法都返回了一个primise<数据>么? 因为第一步firstly {} 这个代码可以省略,所以我们直接调用viewModel的方法,产生了一个promise,提供给下一步(.then)使用
        return Promise<class1>
        }.then { 参数 -> Void in //此处的参数就是上一步Promise提供的class1类型的,如果此处then已经结束操作了,没有下一步了,就不需要给下一步提供promise,要返回一个Void,然后这里可以随便写东西
        }.catch { error in
        // 最后别忘记捕捉error

        }
        范范饭特稀:这里只是简单的应用,具体PromiseKit的使用你看看文档啊!你看看这个错误很明显啊~~~~"missing return in a closure to return anypromise". 每一步(then->then->then)都要返回一个Promise闭包的,要是不返回的话,必须返回一个Void啊,就像这样写:viewModel.getHomepagePageData().then { data -> Void in
        // 你的代码 ,这时候写多行代码就不会报错了.
        }
      • 我是王海龙:“Moya的TargetType协议规定使用枚举来创建网络请求,我们可以通过枚举来区分不同业务的网络请求。”

        这样每个枚举都需要实现TargetType,每个实现里都会出现BaseURL,这样显得不优雅了。请问有好的办法避免吗?
        范范饭特稀:这个TargetType协议是Moya规定的必须实现的,肯定会出现baseUrl,避免不了,你可以设想一下若是请求第三方的服务时,baseurl肯定不是自己的服务器,那么这样看来,这个baseurl还是有必要存在的
        范范饭特稀:这里只是一个实例,你可以定义其他的类来统一管理这些常量,或者像wtqhy14615说的那样写一个extension.比如我就是写了一个类统一管理app相关的东西.
        var baseURL: URL {
        return URL(string: AppManger.sharedInstance.server)!
        }
        wtqhy14615:@让吃货瘦成一道闪电 给TargetType增加一个extension ,实现baseurl
      • Anson3342:有点心动想在新项目中用swift3写了,看起来网络层轻松好多
        范范饭特稀:@Anson3342 是啊,但是要踩很多坑,很多问题自己还没弄明白,只能不断的探索啦~

      本文标题:Moya+PromiseKit+RxSwift优雅的书写网络请求

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