内容概览

  • Moya
  • RxSwift
  • 实例讲解
  • 总结

Moya

/// The protocol used to define the specifications necessary for a `MoyaProvider`.
public protocol TargetType {/// The target's base `URL`.var baseURL: URL { get }/// The path to be appended to `baseURL` to form the full `URL`.var path: String { get }/// The HTTP method used in the request.var method: Moya.Method { get }/// Provides stub data for use in testing.var sampleData: Data { get }/// The type of HTTP task to be performed.var task: Task { get }/// The type of validation to perform on the request. Default is `.none`.var validationType: ValidationType { get }/// The headers to be used in the request.var headers: [String: String]? { get }
}

在遵循这个协议的类型中提供这些属性,可以很方便地定制API请求的各种参数。
而且,由于是属性,所以非常容易进行测试。

/// Request provider class. Requests should be made through this class only.
open class MoyaProvider<Target: TargetType>: MoyaProviderType {/// Closure that defines the endpoints for the provider.public typealias EndpointClosure = (Target) -> Endpoint/// Closure that decides if and what request should be performed.public typealias RequestResultClosure = (Result<URLRequest, MoyaError>) -> Void/// Closure that resolves an `Endpoint` into a `RequestResult`.public typealias RequestClosure = (Endpoint, @escaping RequestResultClosure) -> Void/// Closure that decides if/how a request should be stubbed.public typealias StubClosure = (Target) -> Moya.StubBehavior/// A closure responsible for mapping a `TargetType` to an `EndPoint`.public let endpointClosure: EndpointClosure/// A closure deciding if and what request should be performed.public let requestClosure: RequestClosure/// A closure responsible for determining the stubbing behavior/// of a request for a given `TargetType`.public let stubClosure: StubClosure.../// Initializes a provider.public init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping,stubClosure: @escaping StubClosure = MoyaProvider.neverStub,callbackQueue: DispatchQueue? = nil,manager: Manager = MoyaProvider<Target>.defaultAlamofireManager(),plugins: [PluginType] = [],trackInflights: Bool = false) {self.endpointClosure = endpointClosureself.requestClosure = requestClosureself.stubClosure = stubClosureself.manager = managerself.plugins = pluginsself.trackInflights = trackInflightsself.callbackQueue = callbackQueue}...
}

实际的请求由MoyaProvider发起,而实例化一个MoyaProvider需要提供一个泛型参数Target
所以MoyaProvider需要相应的TargetType,然后才能完成请求。

在进行测试的时候,我们可以使用这些Closure去深度定制MoyaProvider实例以实现我们想要的效果。


RxSwift

基于RxSwift去实现网络请求层的优点:

初次接触 RxSwift 的朋友可以简单了解一下:
Learn & Master ⚔️ the Basics of RxSwift in 10 Minutes


实例讲解

Demo 地址:MoyaWithRxSwiftDemo

  • 实现API,并遵循 TargetType 协议
/// 定义首页模块需要用到的API信息
struct HomeAPI {let baseURL: URLlet endpoint: HomeAPIEndpoint
}/// 多个API请求
enum HomeAPIEndpoint {// 如果需要传递参数,可以使用枚举的关联值,比如:case basicInfo(userID: String)case basicInfo case hobbies
}/// 遵循 TargetType 协议,并定制请求参数
extension HomeAPI: TargetType {var path: String {switch endpoint {case .basicInfo:return "basic_info.json"case .hobbies:return "hobbies.json"}}var method: Moya.Method {// 如果有必要,可以根据 endpoint 来改变return .get}/// 每个请求需要用到的参数var task: Task {// 如果endpoint中的枚举有关联值,可以使用 switch 中的 case let 取出关联值return .requestParameters(parameters: [:], encoding: URLEncoding.queryString)}var headers: [String : String]? { nil }/// 不建议将测试数据放到App所在的Targetvar sampleData: Data { Data() }/// 测试时,如果需要测试状态码,就需要重写这个属性。/// TargetType 协议中的默认实现是返回 .none,也就是不校验 statusCodevar validationType: ValidationType { .successAndRedirectCodes }
}
  • 使用 MoyaProvider 发起请求,并处理回调(采用 Observable 实现)
final class HomeNetworkHelper {private let baseURL: URLprivate let moyaProvider: MoyaProvider<HomeAPI>// moyaProvider 采用依赖注入的方式进行初始化,便于测试init(baseURL: URL,moyaProvider: MoyaProvider<HomeAPI> = MoyaProvider<HomeAPI>()) {self.baseURL = baseURLself.moyaProvider = moyaProvider}func fetchBasicInfo() -> Observable<UserBasicInfo> {return Observable.create { (observer) -> Disposable inlet api = HomeAPI(baseURL: self.baseURL, endpoint: .basicInfo)self.requestAPI(api: api, observer: observer)return Disposables.create()}.observeOn(SerialDispatchQueueScheduler(qos: .default)) // 切换到后台线程}func fetchHobbies() -> Observable<UserHobbies> {return Observable.create { (observer) -> Disposable inlet api = HomeAPI(baseURL: self.baseURL, endpoint: .hobbies)self.requestAPI(api: api, observer: observer)return Disposables.create()}.observeOn(SerialDispatchQueueScheduler(qos: .default))}func requestAPI<T: Decodable>(api: HomeAPI, observer: AnyObserver<T>) {// moyaProvider 发起网络请求self.moyaProvider.request(api) { (response) inswitch response {case .success(let value):do {let result = try value.map(T.self, atKeyPath: nil, using: JSONDecoder(), failsOnEmptyData: false)observer.onNext(result)observer.onCompleted()} catch {observer.onError(error)}case .failure(let error):observer.onError(error)}}}
}
  • 订阅 Observable,处理网络请求回调
class ViewController: UIViewController {...private lazy var networkHelper = HomeNetworkHelper(baseURL: baseURL)private let baseURL = URL(string: "https://raw.githubusercontent.com/FicowShen/MoyaWithRxSwiftDemo/master/MoyaWithRxSwiftDemo/json/")!private let disposeBag = DisposeBag()...func loadFirstRow() {networkHelper.fetchBasicInfo().delay(DispatchTimeInterval.milliseconds(500),scheduler: MainScheduler.instance).observeOn(MainScheduler.instance) // 切换到主线程。如果引入了RxCocoa,可以将Observable转换为Driver.subscribe(onNext: { [unowned self] (model) inself.models.insert(model, at: 0)self.myTableView.reloadData()}, onError: { (error) inlogError(error)}).disposed(by: disposeBag)}}
  • 对网络请求层进行单元测试
import XCTest
import Moya
import RxBlocking
@testable import MoyaWithRxSwiftDemoclass MoyaWithRxSwiftDemoTests: XCTestCase {func testHomeAPI() {guard let url = URL(string: "https://apple.com") else {XCTFail()return}var api = HomeAPI(baseURL: url, endpoint: .basicInfo)XCTAssertEqual(api.path, "basic_info.json")api = HomeAPI(baseURL: url, endpoint: .hobbies)XCTAssertEqual(api.path, "hobbies.json")}/// 测试请求成功的情况func testSuccessfulHomeAPIRequest() {// 从项目中的JSON里读取数据用来模拟请求成功后返回的model数据guard let url = URL(string: "https://apple.com"),let basicInfoData = loadDataInJSONFile(fileName: "basic_info") else {XCTFail()return}// endpointClosure 可以用来修改// 如果 MoyaProvider 的 stubClosure 不为空,则 MoyaProvider 不会发起真的网络请求let provider = MoyaProvider<HomeAPI>(endpointClosure: { self.mockEndpointForAPI(api: $0, response: .networkResponse(200, basicInfoData)) },stubClosure: { _ in .immediate })let apiHelper = HomeNetworkHelper(baseURL: url, moyaProvider: provider)// RxBlocking 提供了便捷的方法,可以阻塞住当前线程,然后收集 Observable 中的事件guard let basicInfo = try? apiHelper.fetchBasicInfo().toBlocking().first() else {XCTFail()return}XCTAssertEqual(basicInfo.name, "John")XCTAssertEqual(basicInfo.age, 10)}/// 测试发生网络故障(请求超时等情况)的情况func testNetworkErrorForHomeAPIRequest() {guard let url = URL(string: "https://apple.com") else {XCTFail()return}let expectedError = NSError(domain: "expectedError", code: -1, userInfo: nil)let provider = MoyaProvider<HomeAPI>(endpointClosure: { self.mockEndpointForAPI(api: $0, response: .networkError(expectedError)) },stubClosure: { _ in .immediate })let apiHelper = HomeNetworkHelper(baseURL: url, moyaProvider: provider)expectError(expectedError) {_ = try apiHelper.fetchBasicInfo().toBlocking().first()}}/// 测试请求成功后,响应为错误的情况func testResponseErrorForHomeAPIRequest() {guard let url = URL(string: "https://apple.com") else {XCTFail()return}let expectedError = NSError(domain: "", code: 404, userInfo: nil)let provider = MoyaProvider<HomeAPI>(endpointClosure: {self.mockEndpointForAPI(api: $0, response: .networkResponse(404, Data())) },stubClosure: { _ in .immediate })let apiHelper = HomeNetworkHelper(baseURL: url, moyaProvider: provider)expectError(expectedError) {_ = try apiHelper.fetchBasicInfo().toBlocking().first()}}func mockEndpointForAPI(api: TargetType, response: EndpointSampleResponse) -> Endpoint {return Endpoint(url: api.baseURL.absoluteString,sampleResponseClosure: { response },method: api.method,task: api.task,httpHeaderFields: api.headers)}func loadDataInJSONFile(fileName: String) -> Data? {let bundle = Bundle(for: type(of: self))guard let filePath = bundle.path(forResource: fileName, ofType: "json"),let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {return nil}return data}func expectError(_ expectedError: NSError, inFailedRequest requestOperation: (() throws -> ())) {do {try requestOperation()XCTFail()} catch let error as Moya.MoyaError {switch error {case let .underlying(error as NSError, response):if let response = response {XCTAssertEqual(expectedError.code, response.statusCode)} else {XCTAssertEqual(error.code, expectedError.code)}default:XCTFail()}} catch {XCTFail()}}}

总结

基于 RxSwift + Moya 搭建的网络请求层,具有以下优势:

  • 可以模块化,易于扩展
  • 职责分明,易于测试
  • 函数式编程,操作简洁、顺畅
  • 线程切换,简洁、高效

劣势:

  • RxSwift 学习成本略高
  • Moya 基于 Alamofire,需要引入一个网络请求库

参考文章:
Moya Tutorial for iOS: Getting Started
Getting Started With RxSwift and RxCocoa
RxSwift + MVVM: how to feed ViewModels
Testing Your RxSwift Code

转载请注明出处,谢谢~

iOS 基于 RxSwift + Moya 搭建易测试的网络请求层相关推荐

  1. 基于RxJava2+Retrofit2简单易用的网络请求实现

    代码地址如下: http://www.demodashi.com/demo/13473.html 简介 基于RxJava2+Retrofit2实现简单易用的网络请求,结合android平台特性的网络封 ...

  2. retrofit 会请求两次_基于RxJava2+Retrofit2简单易用的网络请求实现

    简介 基于RxJava2+Retrofit2实现简单易用的网络请求,结合android平台特性的网络封装库,采用api链式调用一点到底,集成cookie管理,多种缓存模式,极简https配置,上传下载 ...

  3. 一款基于RxJava2+Retrofit2实现简单易用的网络请求框架

    本库是一款基于RxJava2+Retrofit2实现简单易用的网络请求框架,结合android平台特性的网络封装库,采用api链式调用一点到底,集成cookie管理,多种缓存模式,极简https配置, ...

  4. iOS音频的后台播放总结(后台网络请求歌曲,Remote控制,锁屏封面,各种打断)...

    iOS音频的后台播放总结(后台网络请求歌曲,Remote控制,锁屏封面,各种打断) 2013-12-11 21:13 1416人阅读 评论(0) 收藏 举报  分类: cocoa SDK(139)  ...

  5. android搭建网络框架,Android 搭建MVP+Retrofit+RxJava网络请求框架(三)

    上一篇中主要是将mvp+rxjava+retrofit进行了结合,本篇主要是对mvp框架的优化:建议先去看上一篇:Android 搭建MVP+Retrofit+RxJava网络请求框架(二) 针对vi ...

  6. 基于raft共识搭建的Fabric1.4网络环境

    基于Raft共识搭建多机Fabric1.4网络环境 ​ 由于近期fabric官方继fabric1.4LTS版本之后,又推出了fabric1.4.1的正式补丁版本,虽然fabric1.4.1是fabri ...

  7. Android 教你一步步搭建MVP+Retrofit+RxJava网络请求框架

    目录 1.什么是MVP? 2.什么是Retrofit? 3.RxJava 4.实践 之前公司的项目用到了MVP+Retrofit+RxJava的框架进行网络请求,所以今天特此写一篇文章以做总结.相信很 ...

  8. iOS —— 奇葩问题一 iOS15 首次启动app网络请求失败

    背景: 在iOS15上 首次启动app,如图显示本地网络弹框提示,并且此时所有的网络请求都是失败的. 原因 通过不断测试发现是 手机开通代理导致的, 关闭代理后就不会有如图弹框. iOS15之前的系统 ...

  9. 一步步搭建Retrofit+RxJava+MVP网络请求框架(一)

    首先,展示一下封装好之后的项目的层级结构.  1.先创建一个RetrofitApiService.java package com.xdw.retrofitrxmvpdemo.http;import ...

  10. 关于RxSwift MVVM flatMapLatest 点击事件网络请求失败整个序列结束

    例子 先上代码吧: self.signedIn = input.loginTaps.withLatestFrom(usernameAndPassword).flatMapLatest { (usern ...

最新文章

  1. 难道他们说的都是真的?
  2. 【干货】人工智能工程师的三个层次
  3. 【struts2+hibernate+spring项目实战】用户登录校验(struts拦截器)
  4. boost::log::sinks::simple_event_log_backend用法的测试程序
  5. QT的QMapIterator类的使用
  6. 将数据渲染到页面的几种方式
  7. linux中类似findfirst的函数,findfirst函数的用法
  8. 02--Tomcat总体结构分析一
  9. Android提供两个常用的消息弹出框【Toast和Alert】
  10. 联想服务器改win7系统教程,联想笔记本Win10改Win7方法分享
  11. 《机器视觉算法与应用》学习笔记(一)图像采集——照明
  12. java word 分页显示_Java 在Word中插入分页符、分节符
  13. 计算机组成原理头歌实验
  14. 兜兜转转躲不命运轮回---Java基础学习笔记1
  15. linux下exec用法,linux下exec系列(一)
  16. 注册时出现服务器错误,创建Apple ID时出现服务器错误,导致无法完成注册是什么原因...
  17. APS高级计划与排程系统基本概念和功能说明
  18. Android基于红米系列手机读取本地图片路径失败的解决方案
  19. 优麒麟 20.04 LTS Pro 发布 | 以初心,铸匠心
  20. 串行干扰消除matlab仿真,串行干扰消除求详解

热门文章

  1. md5加密依赖工具utility使用小记
  2. 输入下载页面链接自动获取ipa下载地址,支持本地下载,支持蒲公英和fir及绝大多数自定义下载页
  3. php蘑菇街商城源码,php源码:dedecms精仿蘑菇街(mogujie.com)源码,时尚购物社区源码...
  4. struts2配置细节
  5. mybatis <where> <choose>标签
  6. 服务器系统咋关机呀,服务器关机详细步骤
  7. docker 安装 Nginx 并配置反向代理
  8. 思维导图工具之Freeplane(上篇)
  9. 【老九】【Python】文件操作与异常处理
  10. Navicat还原nb3备份文件