美文网首页IOS开发swift开源库常用的第三方
如何写出最简洁优雅的网络封装 Moya + RxSwift

如何写出最简洁优雅的网络封装 Moya + RxSwift

作者: 最Fly的Engine人 | 来源:发表于2016-09-23 18:21 被阅读11538次

前言


  • Why Moya ?

Alamofire可能是iOS Swift中最常用的HTTP networking library,用Alamofire可以抽象出NSURLSession和其中很多繁琐的细节,让你可以很方便地写出类似"APIManager"这种专门管理网络请求的类。

我们可以看一些例子,例子中用的JSONPlaceholder是一个免费的测试用的REST API:

//GET request
let postEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"
Alamofire.request(.GET, postEndpoint) 
  .responseJSON { response in
    //do something with response
  }

//POST request
let postsEndpoint: String = "http://jsonplaceholder.typicode.com/posts"
let newPost = ["title": "title", "body": "body", "userId": 1]
Alamofire.request(.POST, postsEndpoint, parameters: newPost, encoding: .JSON)
  .responseJSON { response in
    //do something with response
  }

对于每个请求,你必须提供一个String类型的URL和一个HTTP请求方法,像.GET,如果你有很多请求需要完成,那会让代码显得不那么容易阅读,维护和测试。
解决这些问题的办法就是利用Swift enum的特性给Alamofire添加一个router,这就是Moya。

  • What is Moya ?

Moya是一个基于Alamofire的Networking library,并且添加了对于ReactiveCocoa和RxSwift的接口支持,大大简化了开发过程,是Reactive Functional Programming的网络层首选。
Github上的官方介绍罗列了Moya的一些特点:

  • 编译的时候会检查API endpoint
  • 可以用枚举值清楚地定义很多endpoint
  • 增加了stubResponse类型,大大方便了unit testing

正文


正文首先介绍如何使用 Moya,第二步为 Moya 添加 RxSwift, 然后再加入数据层的映射(Model Mapping),最后在这个简单的例子中加入MVVM。一步一步地循序渐进,希望对大家有帮助。

Moya

首先创建一个 enum 来枚举你所有的 API targets。你可以把所有关于这个API的信息放在这个枚举类型中。

enum MyAPI {
    case Show
    case Create(title: String, body: String, userId: Int)
}

这个枚举类型用来在编译的阶段给每个target提供具体的信息,每个枚举的值必须有发送http request需要的基本参数,像url,method,parameters等等。这些要求被定义在一个叫做TargetType的协议中,在使用过程过我们的枚举类型需要服从这个协议。通常我们把这一部分的代码写在枚举类型的扩展里。

extension MyAPI: TargetType {
    var baseURL: URL {
        return URL(string: "http://jsonplaceholder.typicode.com")!
    }
    
    var path: String {
        switch self {
        case .Show:
            return "/posts"
        case .Create(_, _, _):
            return "/posts"
        }
    }
    
    var method: Moya.Method {
        switch self {
        case .Show:
            return .GET
        case .Create(_, _, _):
            return .POST
        }
    }
    
    var parameters: [String: Any]? {
        switch self {
        case .Show:
            return nil
        case .Create(let title, let body, let userId):
            return ["title": title, "body": body, "userId": userId]
        }
    }
    
    var sampleData: Data {
        switch self {
        case .Show:
            return "[{\\"userId\\": \\"1\\", \\"Title\\": \\"Title String\\", \\"Body\\": \\"Body String\\"}]".data(using: String.Encoding.utf8)!
        case .Create(_, _, _):
            return "Create post successfully".data(using: String.Encoding.utf8)!
        }
    }
    
    var task: Task {
        return .request
    }
}

Moya的使用非常简单,通过TargetType协议定义好每个target之后,就可以直接使用Moya开始发送网络请求了。

let provider = MoyaProvider<MyAPI>()
        provider.request(.Show) { result in
            // do something with result
        }

+ RxSwift

Moya本身已经是一个使用起来非常方便,能够写出非常简洁优雅的代码的网络封装库,但是让Moya变得更加强大的原因之一还因为它对于Functional Reactive Programming的扩展,具体说就是对于RxSwift和ReactiveCocoa的扩展,通过与这两个库的结合,能让Moya变得更加强大。我选择RxSwift的原因有两个,一个是RxSwift的库相对来说比较轻量级,语法更新相对来说比较少,我之前用过ReactiveCocoa,一些大版本的更新需求重写很多代码,第二个更重要的原因是因为RxSwift背后有整个ReactiveX的支持,里面包括Java,JS,.Net, Swift,Scala,它们内部都用了ReactiveX的逻辑思想,这意味着你一旦学会了其中的一个,以后可以很快的上手ReactiveX中的其他语言。

在我之前的几篇文章中已经写了RxSwift的一些简单上手的教程,不太熟悉RxSwift的朋友大家可以看一看,有个大致的了解。Moya提供了非常方面的RxSwift扩展:

let provider = RxMoyaProvider<MyAPI>()
provider.request(.Show)
    .filterSuccessfulStatusCodes()
    .mapJSON()
    .subscribe(onNext: { (json) in
        //do something with posts
        print(json)
     })
     .addDisposableTo(disposeBag)
  1. RxMoyaProvider是MoyaProvider的子类,是对RxSwift的扩展
  2. filterSuccessfulStatusCodes() 是Moya为RxSwift提供的扩展方法,顾名思义,可以得到成功成功地网络请求,忽略其他的
  3. mapJSON() 也是Moya RxSwift的扩展方法,可以把返回的数据解析成 JSON 格式
  4. subscribe 是一个RxSwift的方法,对经过一层一层处理的 Observable 订阅一个 onNext 的 observer,一旦得到 JSON 格式的数据,就会经行相应的处理
  5. addDisposableTo(disposeBag) 是 RxSwift 的一个自动内存处理机制,跟 ARC 有点类似,会自动清理不需要的对象。

运行程序,我们会得到下列的数据,网络请求的代码原来可以写得如此简洁优雅:

[
  {
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto"
  },
  {
    "userId": 1,
    "id": 2,
    "title": "qui est esse",
    "body": "est rerum tempore vitae\\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\\nqui aperiam non debitis possimus qui neque nisi nulla"
  },
  {
    "userId": 1,
    "id": 3,
    "title": "ea molestias quasi exercitationem repellat qui ipsa sit aut",
    "body": "et iusto sed quo iure\\nvoluptatem occaecati omnis eligendi aut ad\\nvoluptatem doloribus vel accusantium quis pariatur\\nmolestiae porro eius odio et labore et velit aut"
  },
  {
    "userId": 1,
    "id": 4,
    "title": "eum et est occaecati",
    "body": "ullam et saepe reiciendis voluptatem adipisci\\nsit amet autem assumenda provident rerum culpa\\nquis hic commodi nesciunt rem tenetur doloremque ipsam iure\\nquis sunt voluptatem rerum illo velit"
  }, ...

+ Model Mapping

在实际应用过程中网络请求往往紧密连接着数据层(Model),具体地说,在我们的这个例子中,一般我们需要建立一个 Post 类用来统一管理数据,类里面有 id, title, body 等信息,然后把得到的一个个 post 的 JSON 数据映射到 Post 类,也就是数据层(Model)。

我之前最常用 SwiftyJSON 这个库来提取 JSON 中的各种信息,它是 Swift 中最常用的处理 JSON 的第三方库,但是在更新到了 Xcode 8 和 Swift 3 之后,这个库一直都没有更新,所以我使用了另一个 Github 上也有数千个star的库,叫做 ObjectMapper

利用 ObjectMapper,创建 Post 类:

class Post: Mappable {
    var id: Int?
    var title: String?
    var body: String?
    var userId: Int?
    
    
    required init?(map: Map) {
    }
    
    func mapping(map: Map) {
        id <- map["id"]
        title <- map["title"]
        body <- map["body"]
        userId <- map["userId"]
    }
}

详细的 ObjectMapper 教程可以查看它的 Github 主页,我在这里只做简单的介绍。

使用 ObjectMapper ,需要让自己的 Model 类使用 Mappable 协议,这个协议包括两个方法:

required init?(map: Map) {}

func mapping(map: Map) {}

mapping 方法中,用 <- 操作符来处理和映射你的 JSON 数据。

数据类建立好之后,我们还需要为 RxSwift 中的 Observable 写一个简单的扩展方法 mapObject,利用我们写好的 Post 类,一步就把 JSON 数据映射成一个个 post。

可以创建一个名为 Observable+ObjectMapper.swift 的文件:

import Foundation
import RxSwift
import Moya
import ObjectMapper

extension Observable {
    func mapObject<T: Mappable>(type: T.Type) -> Observable<T> {
        return self.map { response in
            //if response is a dictionary, then use ObjectMapper to map the dictionary
            //if not throw an error
            guard let dict = response as? [String: Any] else {
                throw RxSwiftMoyaError.ParseJSONError
            }
            
            return Mapper<T>().map(JSON: dict)!
        }
    }
    
    func mapArray<T: Mappable>(type: T.Type) -> Observable<[T]> {
        return self.map { response in
            //if response is an array of dictionaries, then use ObjectMapper to map the dictionary
            //if not, throw an error
            guard let array = response as? [Any] else {
                throw RxSwiftMoyaError.ParseJSONError
            }
            
            guard let dicts = array as? [[String: Any]] else {
                throw RxSwiftMoyaError.ParseJSONError
            }
            
            return Mapper<T>().mapArray(JSONArray: dicts)!
        }
    }
}

enum RxSwiftMoyaError: String {
    case ParseJSONError
    case OtherError
}

extension RxSwiftMoyaError: Swift.Error { }
  1. mapObject 方法处理单个对象,mapArray 方法处理对象数组。
  2. 如果传进来的数据 response 是一个 dictionary,那么就利用 ObjectMapper 的 map 方法映射这些数据,这个方法会调用你之前在 mapping 方法里面定义的逻辑。
  3. 如果 response 不是一个 dictionary, 那么就抛出一个错误。
  4. 在底部自定义了简单的 Error,继承了 Swift 的 Error 类,在实际应用过程中可以根据需要提供自己想要的 Error。

运行下面的程序:

let provider = RxMoyaProvider<MyAPI>()
provider.request(.Show)
    .filterSuccessfulStatusCodes()
    .mapJSON()
    .mapArray(type: Post.self)
    .subscribe(onNext: { (posts: [Post]) in
        //do something with posts
        print(posts.count)
    })
    .addDisposableTo(disposeBag)

provider.request(.Create(title: "Title 1", body: "Body 1", userId: 1))
    .mapJSON()
    .mapObject(type: Post.self)
    .subscribe(onNext: { (post: Post) in
        //do something with post
        print(post.title!)
    })
    .addDisposableTo(disposeBag)

得到结果:

100
Title 1

+ MVVM

MVVM(Model-View-ViewModel)可以把数据的处理逻辑放到 ViewModel 从而大大减轻了 ViewController 的负担,是 RxSwift 中最常用的架构逻辑。

这个例子中我们可以把从网络请求得到数据的步骤写到 ViewModel 文件里:

import Foundation
import RxSwift
import Moya

class ViewModel {
    private let provider = RxMoyaProvider<MyAPI>()
    
    func getPosts() -> Observable<[Post]> {
        return provider.request(.Show)
            .filterSuccessfulStatusCodes()
            .mapJSON()
            .mapArray(type: Post.self)
    }
    
    func createPost(title: String, body: String, userId: Int) -> Observable<Post> {
        return provider.request(.Create(title: title, body: body, userId: userId))
            .mapJSON()
            .mapObject(type: Post.self)
    }

然后在 ViewController 中调用 ViewModel 的方法:

import UIKit
import RxSwift

class ViewController: UIViewController {
    
    let disposeBag = DisposeBag()
    let viewModel  = ViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        viewModel.getPosts()
            .subscribe(onNext: { (posts: [Post]) in
                //do something with posts
                print(posts.count)
            })
            .addDisposableTo(disposeBag)
        
        viewModel.createPost(title: "Title 1", body: "Body 1", userId: 1)
            .subscribe(onNext: { (post: Post) in
                //do something with post
                print(post.title!)
            })
            .addDisposableTo(disposeBag)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
}

文中这个例子的完整项目放在了Github,大家可以下载参考。

相关文章

网友评论

  • pFruHMXB:作者的文章写自2016年,随着库的更新很多 API 已经发生变化了,所以如果遇到错误,请参考官方文档
  • pFruHMXB:你好, filterSuccessfulStatusCodes 是可以过滤请求成功的结果,看他内部是判断 http 的 statusCode 的,但是如果404之类的这有直接就把程序搞崩溃了,很不健壮呀.
  • CadeCao:json解码都要手动解码吗,可以实现自动化吗
  • 独白melody://应该是Moya扩展RxSwift的部分更新了,拓展Observable的map方法像你那样写不正确了
    //以下是我使用苹果新推出的Codable序列化协议进行拓展的方法
    //Observable+Codable.swift
    import RxSwift
    import Moya
    extension Observable where Element == Response {
    func mapModel<T: Codable>(type: T.Type) -> Observable<T> {
    return self.map({ (response) in
    let data = response.data

    let decoder = JSONDecoder()
    guard let baseModel = try? decoder.decode(BaseModel.self, from: data) else {
    throw SYError.decodeError(data.base64EncodedString())
    }
    guard let otherModel = try? decoder.decode(T.self, from: data) else {
    throw SYError.decodeError(data.base64EncodedString())
    }

    guard baseModel.status else {
    throw SYError.serverError(baseModel.message)
    }
    return otherModel
    })
    }
    }

    enum SYError: Error {
    case decodeError(String)
    case serverError(String)
    }
  • 我叫阿水:使用moya+rx的时候,需要显示“菊花”以及失败错误提示等等这些的控制,你是怎么做的?通过插件?但这样会所有请求都会
  • Caiflower:你好我想问下怎么不走 var parameters: [String: Any]? {} 这里获取参数啊
    PierceDark:看下官方 demo 吧,新版的放到 task 里面了
  • Caiflower:RxMoyaProvider 这个类需要引入什么别的框架吗
  • Aaronn:moya 如何上传图片?
    ripple_k:```
    /// The type of HTTP task to be performed.
    var task: Task {
    switch self {
    case let .AddInquiryUser(_, _, _, _, imageDatas):

    var arr: [MultipartFormData] = []
    for imageData in imageDatas {
    arr.append(MultipartFormData(provider: .data(imageData), name: "file", fileName: "file", mimeType: "multipartFiles"))
    }

    if arr.count == 0 {
    arr.append(MultipartFormData.init(provider: .data(Data()), name: "nil"))
    }

    return .upload(.multipart(arr))
    default:
    return .request
    }
    }
    ```
  • TimberTang:好强大.. :blush:
    最Fly的Engine人:@TimberTang :smile:
  • da27c260cc85:addDisposableTo, 为什么我加上这个以后, 请求就不走了?
    薄呱呱呱:@ArthurChi  是怎么解决的? 我也遇到这个问题了:sweat_smile:
    da27c260cc85:@最Fly的Engine人 多谢,搞定了
    最Fly的Engine人:@ArthurChi 可以看一下你的源代码吗?
  • 范范饭特稀:enum RxSwiftMoyaError: String {
    case ParseJSONError
    case OtherError
    }
    extension RxSwiftMoyaError: Swift.Error { }
    定义了这个东西我不太明白,请求的时候若产生错误,抛出异常了,怎么去处理这个错误呢
    范范饭特稀:@601d03b3eaa6 在订阅这个信号的onError回调里面处理这个异常
    .subscribe(onNext: {

    }, onError: {
    // 处理异常
    })
    .addDisposableTo(disposeBag)
    601d03b3eaa6:请问这个问题你解决了吗, 我也遇到了
  • 1de808f22631:想请问下,除了moya还有其他可以封装Alamofire的么,因为最新的支持swift3的Moya在cocopod的时候需要安装Alamofire,但是现在我们的app还要支持iOS8,没法cocopod 安装Alamofire,也就没法安装Moya了。 :skull:
    1de808f22631:{
    "resultCode":200,
    "resultMsg":"查询成功!",
    "data":[
    {
    "city":"北京",
    "temperature":"8℃~20℃",
    "weather":"晴转霾"
    },
    {
    "city":"南京",
    "temperature":"12℃~21℃",
    "weather":"晴"
    }
    ]
    }
    请问这样的情况怎么去处理?我在网上找到一些但是没看明白
    1de808f22631:@TonyDuan 好的,谢谢,我已经安装上了
    TonyDuan:@余泽锋 官方已经出了支持iOS8的了
  • d80c89a0af81:您好,我有点不是很理解
    enum MyAPI {
    case Show
    case Create(title: String, body: String, userId: Int)
    }
    像这样有两个接口,枚举就有2个值,假如我有100个接口,岂不是要定义100个?,然后在扩展里面,每个属性不是要case100次?
    pFruHMXB:@橙子啊 模块化来说最好是分成多个 API 文件来写
    橙子啊:@love_1984 可以写在一起添加注释区分。也可以分多个文件,例如AccountApi.swift,
    enum AccountAPI {
    case login(name: String, password: String) // 登录
    case register(name: String, password: String) // 注册
    }
    MeAPI.swift
    enum MeAPI {
    case feedback(content: String) // 反馈
    }
  • zone7_:跟Android的rxjava+mvp+retrofit有点类似
  • Jvaeyhcd:有没有Swift2.2的demo呢?
    Jvaeyhcd:@最Fly的Engine人 暂时还没有
    最Fly的Engine人:@Jvaeyhcd 这个demo是Swift3.0的,没有写Swift2.2的demo,但是在这个demo中应该改动不大,你具体碰到什么问题了吗?
  • 07d93406ec39:啊,好厲害。。(認真學習臉
    最Fly的Engine人:@漩渦貓_Lanaya 针对不同的response弹出不同的alert吗?我觉得用Rx比较方便吧,可以试一下
    07d93406ec39:@最Fly的Engine人 我最近遇到的問題就是業務中一個登入的流程是序列化的異步,針對每一個異步的錯誤需要彈出各種Alert,不知道是用這種Rx的方式來做,還是用do try catch的方式來做比較好
    最Fly的Engine人:@漩渦貓_Lanaya 谢谢支持啊,哈哈,一起学习进步

本文标题:如何写出最简洁优雅的网络封装 Moya + RxSwift

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