02、Moya


iOS 中可以使用 URLSession 进行网络请求,但是方便起见,通常会选择使用 Alamofire 这样的著名第三方库。而 Moya 又是基于 Alamofire 的更高一层的网络请求封装抽象层,我们可以直接操作 Moya,然后 Moya 去管理请求,而不用直接和 Alamofire 进行接触。

Moya是一个高度抽象的网络库,他的理念是让你不用关心网络请求的底层的实现细节,只用定义你关心的业务。且Moya采用桥接和组合来进行封装(默认桥接了Alamofire),使得Moya非常好扩展,让你不用修改Moya源码就可以轻易定制。官方给出几个Moya主要优点:

  • 编译时检查API endpoint权限
  • 让你使用枚举定义各种不同Target, endpoints
  • 把stubs当做一等公民对待,因此测试超级简单。

1、Moya的使用

1.1 基础使用

  • (1)Moya的使用分成几步,首先需要先自定义一个枚举类型。
private enum General {
    case uploadMulti(Int,[MultipartFormData]) //上传多张图片
}
  • (2)让枚举的分类遵循Moya的TargetType,按需实现定义的各种get方法
extension General : PatientTargetType {
    /// The path to be appended to `baseURL` to form the full `URL`.
    var path: String {
        switch self {
        case .uploadMulti(let type, _):
            return "/public/upload/\(type)/multiple"
        }
    }
    
    /// The HTTP method used in the request.
    var method: MoyaMethod {
        switch self {
        case .uploadMulti:
            return .post
        }
    }
    var baseURL: URL {
        return URL(string:HttpClient.baseHostURLString)!
    }
    var headers: [String: String]? {
        return HttpClient.headers
    }
    
    /// The parameters to be encoded in the request.
    var parameters: [String: Any]? {
        return nil
    }
    
    /// The method used for parameter encoding.
    var parameterEncoding: ParameterEncoding {
        return HMURLEncoding.default
    }
    
    /// Provides stub data for use in testing.
    var sampleData: Data {
        return "".data(using: .utf8)!
    }
    /// The type of HTTP task to be performed.
    var task: MoyaTask {
        switch self {
        case .uploadMulti(_, let formDatas):
            return MoyaTask.uploadMultipart(formDatas)
        }
    }
}

  • (3)最后创建MoyaProvider对象,泛型允许传入任何你定义的遵循TargetType协议的枚举。
private let generalDataProvider = MoyaProvider<General>()
  • (4)使用MoyaProvider对象发起请求
 generalDataProvider.request(General.uploadMulti(type, formDatas)) { result in
            
        }

1.2 Codable

Codable协议是苹果提供解析数据的协议,在不使用第三方库,如ObjectMapper, SwiftyJson的情况下,将服务器返回的JSON数据转为model。
下面是一个简单的Codable示例:

struct Demo: Codable {
    var name: String
    var age: Int
}

func decode() {
    let jsonString =  "{\"name\":\"zhangsan\", \"age\":15}" // 模拟JSON数据
    let decoder = JSONDecoder()
    let data = jsonString.data(using: .utf8)!
    let model = try! decoder.decode(Demo.self, from: data)
    print(model) // Demo(name: "zhangsan", age: 15)
}

在Moya的Response中已经封装好了对应的处理

 DemoProvider.provider.request(.zen) { (result) in
    switch result {
    case .success(let response):
        if let model = try? response.map(Demo.self) {
            success(model)
        }
    case .failure(let error):
        break
   }
}

如果数据是在JSON的好几个层级中,也可以通过设定keypath获取:

{
    data: {
        name: "test",
        age: 15
    }
}

try? response.map(Demo.self, atKeyPath: "data")

要注意的是这里函数还有一个参数叫做failsOnEmptyData,默认设定为true,如果返回的数据为空,会判定会解析失败。

1.3 如何设置Moya请求头部信息

头部信息的设置在开发过程中很重要,如服务器生成的token,用户唯一标识等。

// MARK: - 设置请求头部信息
let myEndpointClosure = { (target: NetAPIManager) -> Endpoint<NetAPIManager> in
 
 let url = target.baseURL.appendingPathComponent(target.path).absoluteString
 let endpoint = Endpoint<NetAPIManager>(
  url: url,
  sampleResponseClosure: { .networkResponse(200, target.sampleData) },
  method: target.method,
  parameters: target.parameters,
  parameterEncoding: target.parameterEncoding
 )
 
 //在这里设置你的HTTP头部信息
 return endpoint.adding(newHTTPHeaderFields: [
  "Content-Type" : "application/x-www-form-urlencoded",
  "ECP-COOKIE" : ""
  ])
 
}

1.4 如何设置请求超时时间

// MARK: - 设置请求超时时间
let requestClosure = { (endpoint: Endpoint<NetAPIManager>, done: @escaping MoyaProvider<NetAPIManager>.RequestResultClosure) in
 
 guard var request = endpoint.urlRequest else { return }
 
 request.timeoutInterval = 30 //设置请求超时时间
 done(.success(request))
}

1.5 自定义插件

自定义插件必须 PluginType 协议的两个方法willSend与didReceive

//
// MyNetworkActivityPlugin.swift
// NN110
//
// Created by 陈亦海 on 2017/5/10.
// Copyright © 2017年 CocoaPods. All rights reserved.
//
 
import Foundation
import Result
import Moya
 
/// Network activity change notification type.
public enum MyNetworkActivityChangeType {
 case began, ended
}
 
/// Notify a request's network activity changes (request begins or ends).
public final class MyNetworkActivityPlugin: PluginType {
 
 public typealias MyNetworkActivityClosure = (_ change: MyNetworkActivityChangeType, _ target: TargetType) -> Void
 let myNetworkActivityClosure: MyNetworkActivityClosure
 
 public init(newNetworkActivityClosure: @escaping MyNetworkActivityClosure) {
  self.myNetworkActivityClosure = newNetworkActivityClosure
 }
 
 // MARK: Plugin
 
 /// Called by the provider as soon as the request is about to start
 public func willSend(_ request: RequestType, target: TargetType) {
  myNetworkActivityClosure(.began,target)
 }
 
 /// Called by the provider as soon as a response arrives, even if the request is cancelled.
 public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
  myNetworkActivityClosure(.ended,target)
 }
}

1.6 使用自定义插件方法

// MARK: - 自定义的网络提示请求插件
let myNetworkPlugin = MyNetworkActivityPlugin { (state,target) in
 if state == .began {
  //  SwiftSpinner.show("Connecting...")
 
  let api = target as! NetAPIManager
  if api.show {
   print("我可以在这里写加载提示")
  }
 
  if !api.touch {
   print("我可以在这里写禁止用户操作,等待请求结束")
  }
 
  print("我开始请求\(api.touch)")
 
  UIApplication.shared.isNetworkActivityIndicatorVisible = true
 } else {
  //  SwiftSpinner.show("request finish...")
  //  SwiftSpinner.hide()
  print("我结束请求")
  UIApplication.shared.isNetworkActivityIndicatorVisible = false
 
 }
}

2、Moya常见文件解析

2.1 MoyaProvider

2.1.1 MoyaProvider 初始化

MoyaProvider是请求提供者类。只能通过该类发起请求,类的初始化如下

 /// 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
    }

由以上代码可以得知,初始化可传入的参数。

EndpointClosure

EndpointClosure是一个把传入的Target转化为Endpoint对象的闭包。

/// Closure that defines the endpoints for the provider.
    public typealias EndpointClosure = (Target) -> Endpoint

接下来看一下Endpoint对象里面是什么

open class Endpoint {
    public typealias SampleResponseClosure = () -> EndpointSampleResponse

    /// A string representation of the URL for the request.请求的URL的字符串.
    public let url: String

    /// A closure responsible for returning an `EndpointSampleResponse`.stub数据的 response(测试用的)
    public let sampleResponseClosure: SampleResponseClosure

    /// The HTTP method for the request.请求方式.
    public let method: Moya.Method

    /// The `Task` for the request.请求任务.
    public let task: Task

    /// The HTTP header fields for the request.请求头.
    public let httpHeaderFields: [String: String]?

    public init(url: String,
                sampleResponseClosure: @escaping SampleResponseClosure,
                method: Moya.Method,
                task: Task,
                httpHeaderFields: [String: String]?) {

        self.url = url
        self.sampleResponseClosure = sampleResponseClosure
        self.method = method
        self.task = task
        self.httpHeaderFields = httpHeaderFields
    }
    ...
}

可以看出,Endpoint的属性,基本对应TargetType协议对应的get方法,所以才能进行转化,EndpointClosure的作用在于,可以根据业务需求在这里重新定制网络请求,还可以通过 stub进行数据测试,可以看看官方默认的闭包实现。

final class func defaultEndpointMapping(for target: Target) -> Endpoint {
        return Endpoint(
            url: URL(target: target).absoluteString,
            sampleResponseClosure: { .networkResponse(200, target.sampleData) },
            method: target.method,
            task: target.task,
            httpHeaderFields: target.headers
        )
    }
RequestClosure

RequestClosure这个闭包实现就是将Endpoint转化成真正的请求对象URLRequest

 /// Closure that resolves an `Endpoint` into a `RequestResult`.
    public typealias RequestClosure = (Endpoint, @escaping RequestResultClosure) -> Void

看看Moya提供的默认实现

 final class func defaultRequestMapping(for endpoint: Endpoint, closure: RequestResultClosure) {
        do {
            let urlRequest = try endpoint.urlRequest()
            closure(.success(urlRequest))
        } catch MoyaError.requestMapping(let url) {
            closure(.failure(MoyaError.requestMapping(url)))
        } catch MoyaError.parameterEncoding(let error) {
            closure(.failure(MoyaError.parameterEncoding(error)))
        } catch {
            closure(.failure(MoyaError.underlying(error, nil)))
        }
    }

上面代码通过endpoint.urlRequest()获得urlRequest,那我们就看一下urlRequest()的具体实现

 /// Extension for converting an `Endpoint` into a `URLRequest`.
extension Endpoint {
    // swiftlint:disable cyclomatic_complexity
    /// Returns the `Endpoint` converted to a `URLRequest` if valid. Throws an error otherwise.
    public func urlRequest() throws -> URLRequest {
        guard let requestURL = Foundation.URL(string: url) else {
            throw MoyaError.requestMapping(url)
        }

        var request = URLRequest(url: requestURL)
        request.httpMethod = method.rawValue
        request.allHTTPHeaderFields = httpHeaderFields

        switch task {
        case .requestPlain, .uploadFile, .uploadMultipart, .downloadDestination:
            return request
        case .requestData(let data):
            request.httpBody = data
            return request
        case let .requestJSONEncodable(encodable):
            return try request.encoded(encodable: encodable)
        case let .requestCustomJSONEncodable(encodable, encoder: encoder):
            return try request.encoded(encodable: encodable, encoder: encoder)
        case let .requestParameters(parameters, parameterEncoding):
            return try request.encoded(parameters: parameters, parameterEncoding: parameterEncoding)
        case let .uploadCompositeMultipart(_, urlParameters):
            let parameterEncoding = URLEncoding(destination: .queryString)
            return try request.encoded(parameters: urlParameters, parameterEncoding: parameterEncoding)
        case let .downloadParameters(parameters, parameterEncoding, _):
            return try request.encoded(parameters: parameters, parameterEncoding: parameterEncoding)
        case let .requestCompositeData(bodyData: bodyData, urlParameters: urlParameters):
            request.httpBody = bodyData
            let parameterEncoding = URLEncoding(destination: .queryString)
            return try request.encoded(parameters: urlParameters, parameterEncoding: parameterEncoding)
        case let .requestCompositeParameters(bodyParameters: bodyParameters, bodyEncoding: bodyParameterEncoding, urlParameters: urlParameters):
            if let bodyParameterEncoding = bodyParameterEncoding as? URLEncoding, bodyParameterEncoding.destination != .httpBody {
                fatalError("Only URLEncoding that `bodyEncoding` accepts is URLEncoding.httpBody. Others like `default`, `queryString` or `methodDependent` are prohibited - if you want to use them, add your parameters to `urlParameters` instead.")
            }
            let bodyfulRequest = try request.encoded(parameters: bodyParameters, parameterEncoding: bodyParameterEncoding)
            let urlEncoding = URLEncoding(destination: .queryString)
            return try bodyfulRequest.encoded(parameters: urlParameters, parameterEncoding: urlEncoding)
        }
    }
    // swiftlint:enable cyclomatic_complexity
}

现在我们清晰的发现确实是转成URLRequest了,事实上,这也是Moya给你最后的机会了,举个例子,你想设置超时时间,

let  requestClosure = { (endpoint:Endpoint<SHChannelViewModelApiManager>,closure:RequestResultClosure){
  do {
         var urlRequest = try endpoint.urlRequest()
         //设置超时时间,urlRequest的可配置的东西都可以在这配置
         urlRequest.timeoutInterval = 60
         closure(.success(urlRequest))
     } catch MoyaError.requestMapping(let url) {
         closure(.failure(MoyaError.requestMapping(url)))
     } catch MoyaError.parameterEncoding(let error) {
         closure(.failure(MoyaError.parameterEncoding(error)))
     } catch {
         closure(.failure(MoyaError.underlying(error, nil)))
     }
}
}

StubClosure

StubClosure返回了一个StubBehavior的枚举值,它就是让你告诉Moya你是否使用Stub返回数据或者怎样使用Stub返回数据,默认是不返回。

 /// Closure that decides if/how a request should be stubbed.
    public typealias StubClosure = (Target) -> Moya.StubBehavior
/// Controls how stub responses are returned.
public enum StubBehavior {

    /// Do not stub. 不使用Stub返回数据.
    case never

    /// Return a response immediately.立即使用Stub返回数据
    case immediate

    /// Return a response after a delay. 一段时间间隔后使用Stub返回的数据.
    case delayed(seconds: TimeInterval)
}

下面用个例子来总结一下这三个闭包的用法

var sampleData: Data {
return "{'code': 0,'Token':'3764837egfdg8dfg8e93hr93'}".data(using:  String.Encoding.utf8)!
 }
//定义在SHChannelViewModelApiManager外头 
let endPointAction  = {(target: SHChannelViewModelApiManager) ->  Endpoint<SHChannelViewModelApiManager> in
  return Endpoint(
      url: URL(target: target).absoluteString,
      sampleResponseClosure: { .networkResponse(400, target.sampleData) },
      method: target.method,
      task: target.task,
      httpHeaderFields: target.headers
  )
}

//3秒后返回
let stubAction: (_ type: SHChannelViewModelApiManager) ->  Moya.StubBehavior  = { type in
      return Moya.StubBehavior.delayed(seconds: 3)
}

//创建moyaProvider
  let moyaProvider = MoyaProvider<SHChannelViewModelApiManager>(endpointClosure: endPointAction,  stubClosure: stubAction)
 //使用
 moyaProvider.request(SHChannelViewModelApiManager.getChannelList)........

Moya的默认实现是neverStub,当使用immediatelyStub或者是delayedStub,请求网络时就不会走真实的数据,而是返回Target中SimpleData的数据,一般用于测试API返回数据的处理。
delayedStub相对于immediatelyStub指定了延迟时长,单位是秒。

Manager

Manager 就是AlamofireSessionManager

public typealias Manager = Alamofire.SessionManager

final class func defaultAlamofireManager() -> Manager {
    let configuration = URLSessionConfiguration.default
    configuration.httpAdditionalHeaders = Manager.defaultHTTPHeaders

    let manager = Manager(configuration: configuration)
    manager.startRequestsImmediately = false
    return manager
}

Manager是真正用来网络请求的类,Moya自己并不提供Manager类,Moya只是对其他网络请求类进行了简单的桥接(桥接模式)。这么做是为了让调用方可以轻易地定制、更换网络请求的库。比如你不想用Alamofire,可以十分简单的换成其他库。

可以设定Timeout,缓存策略等等

let manager: SessionManager = {
    let configuration = defaultURLSessionConfiguration
    configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
    configuration.timeoutIntervalForRequest = 20
    let trustPolicyManager = ServerTrustPolicyManager(policies:
        [
            "www.baidu.com": ServerTrustPolicy.disableEvaluation
        ]
    )
    let manager = SessionManager(configuration: configuration, serverTrustPolicyManager: trustPolicyManager)
    return manager
}()
callbackQueue

callbackQueue作为回调队列传给Alamofire,如果为nil - 将使用Alamofire默认的,下面是找到的一个用途例子

if let callbackQueue = callbackQueue {
              callbackQueue.async(execute: sendProgress)
          } else {
              sendProgress()
          }

可以指定网络请求返回之后的callback线程。默认所有的请求将会被Alamofire放入background线程中, callback将会在主线程中调用。

plugins

plugins- Moya提供了一个插件机制,使我们可以建立自己的插件类来做一些额外的事情。比如写Log,显示“菊花”等。抽离出Plugin层的目的,就是让Provider职责单一,满足开闭原则。把和自己网络无关的行为抽离。避免各种业务揉在一起不利于扩展,(其实更像一个请求的生命周期,在该插入的地方调用)
plugins是遵守了PluginType的插件,一个provider可以方多个Plugin。

public protocol PluginType {
    /// 在发送request之前,还有机会对request修改
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest

    /// 发送之前调用
    func willSend(_ request: RequestType, target: TargetType)

    /// 接受Response之后,在触发callback之前
    func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType)

    /// 在调用Callback之前,还能修改result
    func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>
}

public extension PluginType {
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest { return request }
    func willSend(_ request: RequestType, target: TargetType) { }
    func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) { }
    func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError> { return result }
}

在Plugin中可以做很多事情

  • 记录网络请求
  • 处理隐藏或者显示网络activity progress
  • 对request进行更多的处理
struct TestPlugin: PluginType {
    //  对request进行更多的处理
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
        var request = request
        if target is GitHub {
            request.timeoutInterval = 5
        }
        return request
    }
    
    // 记录网络请求
    func willSend(_ request: RequestType, target: TargetType) {
        print("start")
        print(request.request?.url ?? "")
    }
    
    // 记录网络请求
    func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
        print("end")
        switch result {
        case .success(let response):
            print("end success")
            print(response.request?.url ?? "")
        case .failure(let error):
            print("end failure")
            print(error)
        }
    }
    
    // 对返回的result进行修改
    func process(_ result: Result<Response, MoyaError>, target: TargetType) -> Result<Response, MoyaError> {
        if case let .failure(error) = result {
            return .failure(MoyaError.underlying(error, nil))
        }
        return result
    }
}

Moya也对Logger,activity等提供了默认实现的Plugin,更多细节就不详细说明了。

trackInflights

trackInflights ,根据代码逻辑可以看出来,这是是否对重复请求情况的处理。其中有一个解释是:是否要跟踪重复网络请求。

 if trackInflights {
         objc_sync_enter(self)//递归锁
         var inflightCompletionBlocks = self.inflightRequests[endpoint]
         inflightCompletionBlocks?.append(pluginsWithCompletion)
         self.inflightRequests[endpoint] = inflightCompletionBlocks
         objc_sync_exit(self)

         if inflightCompletionBlocks != nil {
         // 如果存在,就是说明已经有一个已经重复的请求了,就把这个取消了
             return cancellableToken
         } else {
             objc_sync_enter(self)
             // 如果不存在 key 为 endpoint 的值,则初始化一个
             self.inflightRequests[endpoint] = [pluginsWithCompletion]
             objc_sync_exit(self)
         }
     }

一个请求在 init 的时候将 trackInflights 设置为 true,那么在Moya 中就会存储这个请求的 endpoint。在返回数据的时候,如果需要跟踪了重复请求,那么就将一次实际发送请求返回的数据,多次返回。

2.1.2 MoyaProvider 发送请求

request

在 Moya 中 request 方法是一个统一的请求入口。只需要在方法中配置需要的参数,包括需要对应生成的请求地址,请求参数等通过枚举类型,十分清晰的分类和管理。利用 . 语法生成对应的枚举,然后依次生成 endpoint,URLRequest等。

 /// Designated request-making method. Returns a `Cancellable` token to cancel the request later.
    @discardableResult
    open func request(_ target: Target,
                      callbackQueue: DispatchQueue? = .none,
                      progress: ProgressBlock? = .none,
                      completion: @escaping Completion) -> Cancellable {

        let callbackQueue = callbackQueue ?? self.callbackQueue
        return requestNormal(target, callbackQueue: callbackQueue, progress: progress, completion: completion)
    }
target

target就是传入的自定义枚举。

callbackQueue

同上

progress

progress代表请求任务完成进度的回调,默认不使用

public typealias ProgressBlock = (_ progress: ProgressResponse) -> Void

再点进去ProgressResponse看看


public struct ProgressResponse {

    /// The optional response of the request.
    public let response: Response?

    /// An object that conveys ongoing progress for a given request.
    public let progressObject: Progress?

    /// Initializes a `ProgressResponse`.
    public init(progress: Progress? = nil, response: Response? = nil) {
        self.progressObject = progress
        self.response = response
    }

    /// The fraction of the overall work completed by the progress object.
    public var progress: Double {
        if completed {
            return 1.0
        } else if let progressObject = progressObject, progressObject.totalUnitCount > 0 {
            // if the Content-Length is specified we can rely on `fractionCompleted`
            return progressObject.fractionCompleted
        } else {
            // if the Content-Length is not specified, return progress 0.0 until it's completed
            return 0.0
        }
    }

    /// A Boolean value stating whether the request is completed.
    public var completed: Bool {
        return response != nil
    }
}

由上可知,progressObject是一个Foundation框架的Progress对象,这是 iOS 7加入的专门用于监控任务进度的类

completion

completion是请求完成后返回的回调

public typealias Completion = (_ result: Result<Moya.Response, MoyaError>) -> Void

2.2 MoyaProvider+Defaults

MoyaProvider+Defaults里面就是 3 个默认方法,前面已经提到过,就不多做赘述了

  • defaultEndpointMapping 返回 Endpoint的默认方法
  • defaultRequestMapping 本质是返回URLRequest的默认方法
  • defaultAlamofireManager返回网络库的manager的默认方法(默认是Alamofire)

2.3 MoyaProvider+Internal

Method

Method是对Alamofire.HTTPMethod的拓展,添加supportsMultipart方法来判断,请求方式支不支持多种请求方式一起出现。

extension Method {
    /// A Boolean value determining whether the request supports multipart.
    public var supportsMultipart: Bool {
        switch self {
        case .post, .put, .patch, .connect:
            return true
        case .get, .delete, .head, .options, .trace:
            return false
        }
    }
}
requestNormal

requestNormal是MoyaProvider里request调的方法,方法里说明了,Moya在请求的时候到底做了什么

/// Performs normal requests.
    func requestNormal(_ target: Target, callbackQueue: DispatchQueue?, progress: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> Cancellable {
    //获取endpoint、stubBehavior和初始化cancellableToken
        let endpoint = self.endpoint(target)
        let stubBehavior = self.stubClosure(target)
          //这个类控制是否取消请求任务
        let cancellableToken = CancellableWrapper()
 		// 允许插件修改 response
        // Allow plugins to modify response
        let pluginsWithCompletion: Moya.Completion = { result in
            let processedResult = self.plugins.reduce(result) { $1.process($0, target: target) }
            completion(processedResult)
        }
		//是否追踪重复请求
        if trackInflights {
            objc_sync_enter(self)//递归锁
            var inflightCompletionBlocks = self.inflightRequests[endpoint]
            inflightCompletionBlocks?.append(pluginsWithCompletion)
            self.inflightRequests[endpoint] = inflightCompletionBlocks
            objc_sync_exit(self)

            if inflightCompletionBlocks != nil {
                return cancellableToken  // 如果存在,就是说明已经有一个已经重复的请求了,就把这个取消了
            } else {
                objc_sync_enter(self) 
                // 如果不存在 key 为 endpoint 的值,则初始化一个
                self.inflightRequests[endpoint] = [pluginsWithCompletion]
                objc_sync_exit(self)
            }
        }
//字面上理解,就是真正执行请求的下一步了。这个闭包,是在 endpoint → URLRequest 方法执行完成后的闭包
        let performNetworking = { (requestResult: Result<URLRequest, MoyaError>) in
         // 先判断这个请求是否取消,是则返回错误类型为 cancel 的错误提示数据
            if cancellableToken.isCancelled {
                self.cancelCompletion(pluginsWithCompletion, target: target)
                return
            }

            var request: URLRequest!

            switch requestResult {
            case .success(let urlRequest):
                request = urlRequest
            case .failure(let error):
                pluginsWithCompletion(.failure(error))
                return
            }
 			// 允许插件修改 request
            // Allow plugins to modify request
            let preparedRequest = self.plugins.reduce(request) { $1.prepare($0, target: target) }
// 定义返回结果闭包,这里返回的是请求返回的数据映射成了 Result
            let networkCompletion: Moya.Completion = { result in
              if self.trackInflights {
                self.inflightRequests[endpoint]?.forEach { $0(result) }

                objc_sync_enter(self)
                self.inflightRequests.removeValue(forKey: endpoint)
                objc_sync_exit(self)
              } else {
               // 使用上面的闭包,通知所有插件,且返回结果
                pluginsWithCompletion(result)
              }
            }
  // 这一步就是执行请求的下一步了,将所有参数继续传递
            cancellableToken.innerCancellable = self.performRequest(target, request: preparedRequest, callbackQueue: callbackQueue, progress: progress, completion: networkCompletion, endpoint: endpoint, stubBehavior: stubBehavior)
        }
  // 接下去的就是将上面定义好的两个闭包,传入到 requestClosure 闭包中
        requestClosure(endpoint, performNetworking)

        return cancellableToken
    }
performRequest

performRequest是上面执行请求的下一步,这个方法的内部实现,根据 switch stubBehaviorendpoint.task 来分别执行对应的请求方式。

 // swiftlint:disable:next function_parameter_count
    private func performRequest(_ target: Target, request: URLRequest, callbackQueue: DispatchQueue?, progress: Moya.ProgressBlock?, completion: @escaping Moya.Completion, endpoint: Endpoint, stubBehavior: Moya.StubBehavior) -> Cancellable {
        switch stubBehavior {
        case .never:
            switch endpoint.task {
            case .requestPlain, .requestData, .requestJSONEncodable, .requestCustomJSONEncodable, .requestParameters, .requestCompositeData, .requestCompositeParameters:
                return self.sendRequest(target, request: request, callbackQueue: callbackQueue, progress: progress, completion: completion)
            case .uploadFile(let file):
                return self.sendUploadFile(target, request: request, callbackQueue: callbackQueue, file: file, progress: progress, completion: completion)
            case .uploadMultipart(let multipartBody), .uploadCompositeMultipart(let multipartBody, _):
                guard !multipartBody.isEmpty && endpoint.method.supportsMultipart else {
                    fatalError("\(target) is not a multipart upload target.")
                }
                return self.sendUploadMultipart(target, request: request, callbackQueue: callbackQueue, multipartBody: multipartBody, progress: progress, completion: completion)
            case .downloadDestination(let destination), .downloadParameters(_, _, let destination):
                return self.sendDownloadRequest(target, request: request, callbackQueue: callbackQueue, destination: destination, progress: progress, completion: completion)
            }
        default:
            return self.stubRequest(target, request: request, callbackQueue: callbackQueue, completion: completion, endpoint: endpoint, stubBehavior: stubBehavior)
        }
    }

2.4、Endpoint

EndPoint是Moya的半个内部数据结构,由所用的TargetType生成,它最终被用来生成网络请求。 每个EndPoint 都存储了下面的数据:

/// A string representation of the URL for the request.
public let url: String

/// A closure responsible for returning an `EndpointSampleResponse`. (单元测试)
public let sampleResponseClosure: SampleResponseClosure

/// The HTTP method for the request.
public let method: Moya.Method

/// The `Task` for the request.
public let task: Task

/// The HTTP header fields for the request.
public let httpHeaderFields: [String: String]?
EndPoint的默认实现

在Provider生成时,可以传入endpointClosure,自定义TargetType到Endpoint的方式。
默认的实现方式:

final class func defaultEndpointMapping(for target: Target) -> Endpoint {
    return Endpoint(
        url: URL(target: target).absoluteString,
        sampleResponseClosure: { .networkResponse(200, target.sampleData) },
        method: target.method,
        task: target.task,
        httpHeaderFields: target.headers
    )
}

在这里可以重新定义Endpoint的生成方式, 比如:

// 将所有生成Endpoint改为get方式请求
let endpointClosure = { (target: MyTarget) -> Endpoint in
    let url = URL(target: target).absoluteString
    return Endpoint(url: url, sampleResponseClosure: {.networkResponse(200, target.sampleData)}, method: .get, task: target.task)
}

或者对已经生成的Endpoint修改:

let endpointClosure = { (target: MyTarget) -> Endpoint in
    let defaultEndpoint = MoyaProvider.defaultEndpointMapping(for: target)
    return defaultEndpoint.adding(newHTTPHeaderFields: ["APP_NAME": "MY_AWESOME_APP"])
}

注意:如果直接对已经初始化的Endpoint修改,只能修改task以及添加header。

Endpoint初始化

Endpoint的初始化方法

  public init(url: String,
                sampleResponseClosure: @escaping SampleResponseClosure,
                method: Moya.Method,
                task: Task,
                httpHeaderFields: [String: String]?) {

        self.url = url
        self.sampleResponseClosure = sampleResponseClosure
        self.method = method
        self.task = task
        self.httpHeaderFields = httpHeaderFields
    }
adding

adding用于创建一个新的Endpoint的便利方法,其属性与接收方相同,但增加了HTTP请求头。

 /// Convenience method for creating a new `Endpoint` with the same properties as the receiver, but with added HTTP header fields.
    open func adding(newHTTPHeaderFields: [String: String]) -> Endpoint {
        return Endpoint(url: url, sampleResponseClosure: sampleResponseClosure, method: method, task: task, httpHeaderFields: add(httpHeaderFields: newHTTPHeaderFields))
    }
replacing
replacing方法差不多,只是更换了Task
urlRequest

urlRequest()方法 转换Endpoint成URLRequest

2.5 TargetType

TargetType就是用于定义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 }

   /// A Boolean value determining whether the embedded target performs Alamofire validation. Defaults to `false`.
   ///是否执行Alamofire校验 ,一般有手机号、邮箱号校验之类的
   var validate: Bool { get }

   /// The headers to be used in the request.
   var headers: [String: String]? { get }
}

2.6 Response

Response就是对对MoyaProvider.request的响应

Response的初始化
 public init(statusCode: Int, data: Data, request: URLRequest? = nil, response: HTTPURLResponse? = nil) {
        self.statusCode = statusCode //状态码
        self.data = data //返回的二进制数据
        self.request = request //URL 请求
        self.response = response //http 的 response
    }

除此之外,还有自带的mapJSON和mapString用于将data转成 JSON或者字符串,map<D: Decodable>自带转模型,可以讲数据转换为签订Decodable类的对象,mapImage在返回 data数据为一个图片二进制数据时使用,直接转换成图片对象返回

 func mapImage() throws -> Image {
     guard let image = Image(data: data) else {
         throw MoyaError.imageMapping(self)
     }
     return image
 }
 
   func mapJSON(failsOnEmptyData: Bool = true) throws -> Any {
     do {
         return try JSONSerialization.jsonObject(with: data, options: .allowFragments)
     } catch {
         if data.count < 1 && !failsOnEmptyData {
             return NSNull()
         }
         throw MoyaError.jsonMapping(self)
     }
 }
 
     public func mapString(atKeyPath keyPath: String? = nil) throws -> String {
     if let keyPath = keyPath {
         // Key path was provided, try to parse string at key path
         guard let jsonDictionary = try mapJSON() as? NSDictionary,
             let string = jsonDictionary.value(forKeyPath: keyPath) as? String else {
                 throw MoyaError.stringMapping(self)
         }
         return string
     } else {
         // Key path was not provided, parse entire response as string
         guard let string = String(data: data, encoding: .utf8) else {
             throw MoyaError.stringMapping(self)
         }
         return string
     }
 }
 
   func map<D: Decodable>(_ type: D.Type, atKeyPath keyPath: String? = nil, using decoder: JSONDecoder = JSONDecoder()) throws -> D {
     let serializeToData: (Any) throws -> Data? = { (jsonObject) in
         guard JSONSerialization.isValidJSONObject(jsonObject) else {
             return nil
         }
         do {
             return try JSONSerialization.data(withJSONObject: jsonObject)
         } catch {
             throw MoyaError.jsonMapping(self)
         }
     }
     let jsonData: Data
     if let keyPath = keyPath {
         guard let jsonObject = (try mapJSON() as? NSDictionary)?.value(forKeyPath: keyPath) else {
             throw MoyaError.jsonMapping(self)
         }

         if let data = try serializeToData(jsonObject) {
             jsonData = data
         } else {
             let wrappedJsonObject = ["value": jsonObject]
             let wrappedJsonData: Data
             if let data = try serializeToData(wrappedJsonObject) {
                 wrappedJsonData = data
             } else {
                 throw MoyaError.jsonMapping(self)
             }
             do {
                 return try decoder.decode(DecodableWrapper<D>.self, from: wrappedJsonData).value
             } catch let error {
                 throw MoyaError.objectMapping(error, self)
             }
         }
     } else {
         jsonData = data
     }
     do {
         return try decoder.decode(D.self, from: jsonData)
     } catch let error {
         throw MoyaError.objectMapping(error, self)
     }
 }

2.7 Plugin

Moya Plugin接收回调(调用时机都是在MoyaProvider+Internal里),以在发送或接收请求时执行。例如,一个插件可以用于

  • 1.记录网络请求

  • 2.隐藏和显示网络活动指示器

  • 3.在请求中注入附加信息
    请添加图片描述

prepare

prepare可以用来修改发送前的请求。(在 stub 测试之前)

func prepare(_ request: URLRequest, target: TargetType) -> URLRequest
willSend

willSend在网络请求发送前调用(在 stub 测试之后)

  func willSend(_ request: RequestType, target: TargetType)
didReceive

didReceive 在收到响应后,但在MoyaProvider调用其完成处理程序之前调用。

func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType)
process

process在 completion前调用用来修改result

 func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>
RequestType

RequestType是willSend需要传入的参数,它的设计遵循迪米特法则(最少知道),我们使用这个协议来代替Alamofire请求,以避免泄露这个抽象概念。Plugin应该是完全不知道Alamofire 的

public protocol RequestType {

  /// Retrieve an `NSURLRequest` representation.
  var request: URLRequest? { get }

  /// Authenticates the request with a username and password.
  func authenticate(user: String, password: String, persistence: URLCredential.Persistence) -> Self

  /// Authenticates the request with an `NSURLCredential` instance.
  func authenticate(usingCredential credential: URLCredential) -> Self
 }

2.8 AccessTokenPlugin

AccessTokenPlugin可用于做JWT的 Bearer 认证 和 Basic 认证,也可以做OAuth认证,不过比较麻烦
必要的时候通过prepare添加授权请求头来验证

 public func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
    guard let authorizable = target as? AccessTokenAuthorizable else { return request }

    let authorizationType = authorizable.authorizationType

    var request = request

    switch authorizationType {
    case .basic, .bearer:
    //添加Authorization,tokenClosure返回一个可应用请求头上的access token
        let authValue = authorizationType.rawValue + " " + tokenClosure()
        request.addValue(authValue, forHTTPHeaderField: "Authorization")
    case .none:
        break
    }

    return request
}

2.9 CredentialsPlugin

AccessTokenPlugin是做 HTTP 身份验证的,在 willSend里验证

 public func willSend(_ request: RequestType, target: TargetType) {
  //credentialsClosure返回一个 URLCredential对象,用于身份验证的系统 api
      if let credentials = credentialsClosure(target) {
      //通过Moya+Alamofire的“extension Request: RequestType { }” 可知,这个authenticate方法最后还是调用的 Alamofire的的认证方法
          _ = request.authenticate(usingCredential: credentials)
      }
  }

3.10 NetworkActivityPlugin

NetworkActivityPlugin还是比较简单的,就是单纯的抽离willSend和didReceive转变成NetworkActivityChangeType的began和ended,可以添加菊花的显示和隐藏

   public func willSend(_ request: RequestType, target: TargetType) {
      networkActivityClosure(.began, target)
  }

  /// Called by the provider as soon as a response arrives, even if the request is canceled.
  public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
      networkActivityClosure(.ended, target)
  }

3.11 NetworkLoggerPlugin

NetworkLoggerPlugin是网络日志的打印,还是在willSend和didReceive打印了网络状态

   public func willSend(_ request: RequestType, target: TargetType) {
     if let request = request as? CustomDebugStringConvertible, cURL {
         output(separator, terminator, request.debugDescription)
         return
     }
     outputItems(logNetworkRequest(request.request as URLRequest?))
 }

 public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
     if case .success(let response) = result {
         outputItems(logNetworkResponse(response.response, data: response.data, target: target))
     } else {
         outputItems(logNetworkResponse(nil, data: nil, target: target))
     }
 }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值