美文网首页swift学习专题
iOS 基于 RxSwift + Moya 搭建易测试的网络请求

iOS 基于 RxSwift + Moya 搭建易测试的网络请求

作者: FicowShen | 来源:发表于2019-11-20 15:34 被阅读0次

     

    内容概览

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

     


    Moya

     

    • TargetType
    /// 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请求的各种参数。
    而且,由于是属性,所以非常容易进行测试。

     

    • MoyaProvider
    /// 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 = endpointClosure
            self.requestClosure = requestClosure
            self.stubClosure = stubClosure
            self.manager = manager
            self.plugins = plugins
            self.trackInflights = trackInflights
            self.callbackQueue = callbackQueue
        }
    
        ...
    }
    

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

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

     


    RxSwift

    基于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: URL
        let 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所在的Target
        var sampleData: Data { Data() }
    
        /// 测试时,如果需要测试状态码,就需要重写这个属性。
        /// TargetType 协议中的默认实现是返回 .none,也就是不校验 statusCode
        var validationType: ValidationType { .successAndRedirectCodes }
    }
    

     

    • 使用 MoyaProvider 发起请求,并处理回调(采用 Observable 实现)
    final class HomeNetworkHelper {
    
        private let baseURL: URL
        private let moyaProvider: MoyaProvider<HomeAPI>
    
        // moyaProvider 采用依赖注入的方式进行初始化,便于测试
        init(baseURL: URL,
             moyaProvider: MoyaProvider<HomeAPI> = MoyaProvider<HomeAPI>()) {
            self.baseURL = baseURL
            self.moyaProvider = moyaProvider
        }
    
        func fetchBasicInfo() -> Observable<UserBasicInfo> {
            return Observable.create { (observer) -> Disposable in
                let 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 in
                let 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) in
                switch 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) in
                    self.models.insert(model, at: 0)
                    self.myTableView.reloadData()
                }, onError: { (error) in
                    logError(error)
                }).disposed(by: disposeBag)
        }
    
    }
    

     

    • 对网络请求层进行单元测试
    import XCTest
    import Moya
    import RxBlocking
    @testable import MoyaWithRxSwiftDemo
    
    class 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 搭建易测试的网络请求

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