iOS 基于 RxSwift + Moya 搭建易测试的网络请求层
内容概览
- 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内置许多操作符,封装了许多常用的处理逻辑)- 使用 Scheduler 切换线程,安全、高效、简洁
- 为以后全项目实现 MVVM with 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 搭建易测试的网络请求层相关推荐
- 基于RxJava2+Retrofit2简单易用的网络请求实现
代码地址如下: http://www.demodashi.com/demo/13473.html 简介 基于RxJava2+Retrofit2实现简单易用的网络请求,结合android平台特性的网络封 ...
- retrofit 会请求两次_基于RxJava2+Retrofit2简单易用的网络请求实现
简介 基于RxJava2+Retrofit2实现简单易用的网络请求,结合android平台特性的网络封装库,采用api链式调用一点到底,集成cookie管理,多种缓存模式,极简https配置,上传下载 ...
- 一款基于RxJava2+Retrofit2实现简单易用的网络请求框架
本库是一款基于RxJava2+Retrofit2实现简单易用的网络请求框架,结合android平台特性的网络封装库,采用api链式调用一点到底,集成cookie管理,多种缓存模式,极简https配置, ...
- iOS音频的后台播放总结(后台网络请求歌曲,Remote控制,锁屏封面,各种打断)...
iOS音频的后台播放总结(后台网络请求歌曲,Remote控制,锁屏封面,各种打断) 2013-12-11 21:13 1416人阅读 评论(0) 收藏 举报 分类: cocoa SDK(139) ...
- android搭建网络框架,Android 搭建MVP+Retrofit+RxJava网络请求框架(三)
上一篇中主要是将mvp+rxjava+retrofit进行了结合,本篇主要是对mvp框架的优化:建议先去看上一篇:Android 搭建MVP+Retrofit+RxJava网络请求框架(二) 针对vi ...
- 基于raft共识搭建的Fabric1.4网络环境
基于Raft共识搭建多机Fabric1.4网络环境 由于近期fabric官方继fabric1.4LTS版本之后,又推出了fabric1.4.1的正式补丁版本,虽然fabric1.4.1是fabri ...
- Android 教你一步步搭建MVP+Retrofit+RxJava网络请求框架
目录 1.什么是MVP? 2.什么是Retrofit? 3.RxJava 4.实践 之前公司的项目用到了MVP+Retrofit+RxJava的框架进行网络请求,所以今天特此写一篇文章以做总结.相信很 ...
- iOS —— 奇葩问题一 iOS15 首次启动app网络请求失败
背景: 在iOS15上 首次启动app,如图显示本地网络弹框提示,并且此时所有的网络请求都是失败的. 原因 通过不断测试发现是 手机开通代理导致的, 关闭代理后就不会有如图弹框. iOS15之前的系统 ...
- 一步步搭建Retrofit+RxJava+MVP网络请求框架(一)
首先,展示一下封装好之后的项目的层级结构. 1.先创建一个RetrofitApiService.java package com.xdw.retrofitrxmvpdemo.http;import ...
- 关于RxSwift MVVM flatMapLatest 点击事件网络请求失败整个序列结束
例子 先上代码吧: self.signedIn = input.loginTaps.withLatestFrom(usernameAndPassword).flatMapLatest { (usern ...
最新文章
- 难道他们说的都是真的?
- 【干货】人工智能工程师的三个层次
- 【struts2+hibernate+spring项目实战】用户登录校验(struts拦截器)
- boost::log::sinks::simple_event_log_backend用法的测试程序
- QT的QMapIterator类的使用
- 将数据渲染到页面的几种方式
- linux中类似findfirst的函数,findfirst函数的用法
- 02--Tomcat总体结构分析一
- Android提供两个常用的消息弹出框【Toast和Alert】
- 联想服务器改win7系统教程,联想笔记本Win10改Win7方法分享
- 《机器视觉算法与应用》学习笔记(一)图像采集——照明
- java word 分页显示_Java 在Word中插入分页符、分节符
- 计算机组成原理头歌实验
- 兜兜转转躲不命运轮回---Java基础学习笔记1
- linux下exec用法,linux下exec系列(一)
- 注册时出现服务器错误,创建Apple ID时出现服务器错误,导致无法完成注册是什么原因...
- APS高级计划与排程系统基本概念和功能说明
- Android基于红米系列手机读取本地图片路径失败的解决方案
- 优麒麟 20.04 LTS Pro 发布 | 以初心,铸匠心
- 串行干扰消除matlab仿真,串行干扰消除求详解
热门文章
- md5加密依赖工具utility使用小记
- 输入下载页面链接自动获取ipa下载地址,支持本地下载,支持蒲公英和fir及绝大多数自定义下载页
- php蘑菇街商城源码,php源码:dedecms精仿蘑菇街(mogujie.com)源码,时尚购物社区源码...
- struts2配置细节
- mybatis <where> <choose>标签
- 服务器系统咋关机呀,服务器关机详细步骤
- docker 安装 Nginx 并配置反向代理
- 思维导图工具之Freeplane(上篇)
- 【老九】【Python】文件操作与异常处理
- Navicat还原nb3备份文件