Swift:网络请求封装库——Moya

8,108 阅读5分钟

这是我参与更文挑战的第13天,活动详情查看: 更文挑战

什么是Moya?

Moya其实没有什么特别神秘的地方,就如我上篇讲Alamofire说的,它就是对Alamofire的一个封装层,不过能把网络请求封装做到这个份上,也可以说明其牛逼之处了。

封装做到极致,就是这样吧。

image.png star高达13.1K个,一般人可以做到么?

为什么是Moya?

这里我就不得不引用官方这段话了:

你是个聪明的开发者。你可能使用 Alamofire 来抽象对 URLSession 的访问,以及所有那些你并不关心的糟糕细节。但是接下来,就像许多聪明开发者一样,你编写专有的网络抽象层,它们可能被称作 "APIManager" 或 "NetworkModel",它们的下场总是很惨。

diagram.png

所以Moya的基本思想是,提供一些网络抽象层,它们经过充分地封装,并直接调用 Alamofire。它们应该足够简单,可以很轻松地应对常见任务,也应该足够全面,应对复杂任务也同样容易。

早在我刚刚从事iOS的时候,前辈写的各种NetworkTool、HttpUtils都让我膜拜的不行,不过在随着编写的代码越多,接触不同的业务,我越来越觉得Moya里面说的那些问题,真的是痛点与难点:

  • 写的网络层只能匹配某一个App,因为其中糅合App中过多的逻辑。

  • 网络层有的时候不能匹配所有的业务,导致针对某个接口需要单独写一个Alamofire请求。

  • 解耦进行组件化效果达不到预期效果,我自己也尝试对网络层进行封装,感觉自己穷尽了自己的洪荒之力,然后发现还是有些遗漏的地方,不尽人意。

正所谓三个臭皮匠赛过诸葛亮,更何况是集众多GitHub大牛之力编写的Moya呢?

Moya的使用

  • 首先我想说的是Moya非常友好,有官方中文文档

  • 再来就是Moya的编程是面向协议的,一旦你遵守了TargetType协议,那么里面的方法就必须实现,所以说编码的严格性,直接让你少犯了不少错。

这里我会以玩安卓的两个接口为例子进行说明,请注意看代码里面的注释:

整理请求的Api

struct Api {
    /// baseUrl
    static let baseUrl = "https://www.wanandroid.com/"
    
    private init() {}

    /// 公众号 均是get请求
    enum PublicNumber {
        static let tags = "wxarticle/chapters/json"

        static let tagList = "wxarticle/list/"
    }
}
  • 业务Provider编写
import Foundation

import Moya

/// 生成一个公众号Provider
let publicNumberProvider: MoyaProvider<PublicNumberService> = {
        let stubClosure = { (target: PublicNumberService) -> StubBehavior in
            return .never
        }
        return MoyaProvider<PublicNumberService>(stubClosure: stubClosure, plugins: [RequestLoadingPlugin()])
}()

/// 编写公众号服务,注意使用的enum,两个接口服务
enum PublicNumberService {
    /// 获取所有的标签
    case tags
    
    /// 单个标签,通过标签id和页码进行请求与上拉加载更多
    case tagList(_ id: Int, _ page: Int)
}

/// 遵守Moya中的TargetType协议
extension PublicNumberService: TargetType {
    
    /// 返回baseURL
    var baseURL: URL {
        return URL(string: Api.baseUrl)!
    }
    
    /// 根据枚举,获取不同服务的Api,这里使用enum带参的知识点
    var path: String {
        switch self {
        case .tags:
            return Api.PublicNumber.tags
        case .tagList(let id, let page):
            return Api.PublicNumber.tagList + id.toString + "/" + page.toString + "/json"
        }
    }
    
    /// 根据枚举,不同的服务使用不同的请求方式,由于这两个服务都是get请求,所以没有写switch case语句
    var method: Moya.Method {
        return .get
    }
    
    /// mock数据,调试的使用,建议使用Swift5的特性,#字符串#,这样写JSON字符串更清爽
    var sampleData: Data {
        return Data()
    }
    
    /// 具体请求任务,不同的服务使用任务,由于都没有传参,所以没有写switch case语句
    var task: Task {
        return .requestParameters(parameters: [:], encoding: URLEncoding.default)
        
    }
    
    /// 请求头,一般情况下,同种服务会有统一的请求头,这里没有涉及所以使用的是nil
    var headers: [String : String]? {
        return nil
    }
}

其实在使用Moya前,理顺自己App中请求接口,分类不同的服务非常重要,找到共同点归类在一起,才能更能体现其价值。

由于我使用的RxMoya,所以调用起来就是下面的例子:

publicNumberProvider.rx.request(PublicNumberService.tags).map(BaseModel<[Tab]>.self)

publicNumberProvider.rx.request(PublicNumberService.tagList(id, page)).map(BaseModel<Page<Info>>.self)

想要看一般的Moya例子,看官方文档即可:

provider = MoyaProvider<GitHub>()
provider.request(.zen) { result in
    switch result {
    case let .success(moyaResponse):
        let data = moyaResponse.data
        let statusCode = moyaResponse.statusCode
        // do something with the response data or statusCode
    case let .failure(error):
        // this means there was a network failure - either the request
        // wasn't sent (connectivity), or no response was received (server
        // timed out).  If the server responds with a 4xx or 5xx error, that
        // will be sent as a ".success"-ful response.
    }
}

Moya同样支持Codable解析JSON,并使用Result返回,是不是觉得通过Moya这节,感觉到了Swift中的enum魅力呢?

  • 插件 通过遵守Moya的PluginType协议,我们可以对不同的服务定制不同的插件以满足请求前响应后的一些预处理,感觉是不是特别向其他语言的拦截器功能,下面就是从网上借鉴过来的插件:
import Moya
import MBProgressHUD

class RequestLoadingPlugin: PluginType {
    
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
        print("prepare")
        var mRequest = request
        mRequest.timeoutInterval = 20
        return mRequest
    }
    
    func willSend(_ request: RequestType, target: TargetType) {
        print("开始请求")
        DispatchQueue.main.async {
            MBProgressHUD.beginLoading()
        }
    }
    
    func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
        print("结束请求")
        // 关闭loading
        DispatchQueue.main.async {
            MBProgressHUD.stopLoading()
        }
        
        switch result {
        case .success(let response):
            if response.statusCode == 200 {
                if let json = try? JSONSerialization.jsonObject(with: response.data, options: .mutableContainers),
                   let data = try? JSONSerialization.data(withJSONObject: json, options: .prettyPrinted),
                   let _ = try? String(data: data, encoding: .utf8) {
                    print(json)
                }
            }else {
                DispatchQueue.main.async {
                    // 进行统一弹窗
                    MBProgressHUD.showText("statusCode not 200")
                }
            }
            
            
        case .failure(let error):
            print(error.localizedDescription)
        }
    }
    
    func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError> {
        return result
    }
}

插件的功能很简单,就是在请求开始loading,在请求后结束loading。

这块我对MBProgressHUD做了一点封装,大家可以自由发挥。

总结

  • 严格的编写模式
  • 简单的调用方式
  • 清晰的结果回调,
  • 灵活的插件组合

种种这些足以让你对不同业务的需求,一旦你试了Moya,我想你会摒弃那些所谓的网络封装层,通过Moya做到大统一吧。

明日继续

端午节第二天更文,明天打算讲一个小技巧,虽然不实用但是有意思,大家加油。