首页 Moya框架浅析
文章
取消

Moya框架浅析

logo

关于Moya

Moya是一个网络抽象层,它在底层将Alamofire进行封装,对外提供更简洁的接口供开发者调用。在以往的Objective-C中,大部分开发者会使用AFNetworking进行网络请求,当业务复杂一些时,会对AFNetworking进行二次封装,编写一个适用于自己项目的网络抽象层。在Objective-C中,有著名的YTKNetwork,它将AFNetworking封装成抽象父类,根据不同的网络请求编写不同的子类,子类继承父类来实现请求业务。Moya在项目层次中的地位,有点类似于YTKNetwork。但是Moya的设计思路和YTKNetwork差距非常大。因为YTKNetwork是比较经典的利用OOP思想(面向对象)设计的产物。而Moya虽然也有使用到继承,但是它的整体上是以POP思想(Protocol Oriented Programming,面向协议编程)为主导的。

从上图可以看出App并不直接与Alamofire进行交互,所有的网络请求都是通过Moya发起的。官方给出Moya的主要优点:

  • 编译时检查API endpoint权限,检测是否使用正确。
  • 通过enum枚举的关联值Targetendpoints定义清晰的用法。
  • 测试stubs为一等数值,让单元测试变得更加简单。

Moya模块组成

由于Moya是使用POP面向协议来设计的一个网络抽象层,它整体的逻辑结构并没有明显的继承关系。Moya的核心代码,可以分成以下几个模块:

Moya类型组成

在使用Moya进行网络请求时,需要进行配置来生成一个RequestRequest的生成过程如下图:

我们根据上图,TargetType是用来提供给开发者用于自定义各个接口的参数。

TargetType

TargetType结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public protocol TargetType {

  /// The target's base `URL`.
  /// 设置通用url
  var baseURL: URL { get }

  /// The path to be appended to `baseURL` to form the full `URL`.
  /// path会追加到baseURL,构成完整的请求URL
  var path: String { get }

  /// The HTTP method used in the request.
  /// HTTP请求类型设置,内部是Alamofire.HTTPMethod
  /// 如.post .get .put
  var method: Moya.Method { get }

  /// Provides stub data for use in testing.
  /// 提供测试所需要的所有数据
  var sampleData: Data { get }

  /// The type of HTTP task to be performed.
  /// 所需要执行的HTTP任务
  /// 如何发送/接收数据,如何添加数据,文件和数据流到请求的 body 中
  var task: Task { get }

  /// A Boolean value determining whether the embedded target    performs Alamofire validation. Defaults to `false`.
  /// 用于验证请求时返回的状态码的类型,默认.none
  var validate: Bool { get }

  /// The headers to be used in the request.
  /// 用于设置请求的请求头
  var headers: [String: String]? { get }
}

Endpoint

Endpoint是由TargetType演变而来的,它是TargetURLRequest的中间态,可以直接生成URLRequest、设置HTTPHeader和SampleResponseClosure,结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
open class Endpoint {
    public typealias SampleResponseClosure = () -> EndpointSampleResponse

  /// 请求URL,用于生成URLRequest的string
  public let url: String

  /// stubs测试时返回的数据 `EndpointSampleResponse`
  public let sampleResponseClosure: SampleResponseClosure

  /// 请求方式,内部其实是 Alamofire.HTTPMethod
  /// 如.post .get .put
  public let method: Moya.Method

  /// 所需要执行HTTP请求任务
  public let task: Task

  /// 用于设置请求头headers
  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进行数据测试,可以看看官方默认的闭包实现:

1
2
3
4
5
6
7
8
9
 public final class func defaultEndpointMapping(for target: Target) -> Endpoint<Target> {
     return Endpoint(
         url: URL(target: target).absoluteString,
         sampleResponseClosure: { .networkResponse(200, target.sampleData) },
         method: target.method,
         task: target.task,
         httpHeaderFields: target.headers
     )
 }

MoyaProvider

  • MoyaProvider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
open class MoyaProvider<Target: TargetType>: MoyaProviderType {

  /// 用于传入的Target转化为Endpoint对象的闭包
  public typealias EndpointClosure = (Target) -> Endpoint

  /// 用于判断URLRequest是否需要执行和对URLRequest进行哪些设置的闭包
    public typealias RequestResultClosure = (Result<URLRequest, MoyaError>) -> Void
	  
  /// 用于将Endpoint转化成真正的请求对象URLRequest闭包
  public typealias RequestClosure = (Endpoint, @escaping RequestResultClosure) -> Void
    
  /// 返回StubBehavior的枚举值
  /// 用于判断Target返回数据和怎样使用stub返回数据
  public typealias StubClosure = (Target) -> Moya.StubBehavior
  
  public let endpointClosure: EndpointClosure
  public let requestClosure: RequestClosure
  public let stubClosure: StubClosure
  
  /// Alamofire的session,用于发起请求
  public let session: Session

  /// 插件,通过插件机制可以做额外的事情,比如Log、数据加载...
  public let plugins: [PluginType]
	  
	/// 是否过滤重复的请求,如果有重复的请求在处理中,就不会发起新的请求而是把 completion添加到对应的inflightCompletionBlocks中,根据Endpoint来判断是否为重复的请求
  public let trackInflights: Bool

  open internal(set) var inflightRequests: [Endpoint: [Moya.Completion]] = [:]
    
  /// 与 Alamfire 的 callbackQueue 进行隔离,如果没有定义就会调用 Alamofire 默认的 queue ( main queue )
  let callbackQueue: DispatchQueue?

  let lock: NSRecursiveLock = NSRecursiveLock()
  public init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,
                requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping,
                stubClosure: @escaping StubClosure = MoyaProvider.neverStub,
                callbackQueue: DispatchQueue? = nil,
                session: Session = MoyaProvider<Target>.defaultAlamofireSession(),
                plugins: [PluginType] = [],
                trackInflights: Bool = false) {
      self.endpointClosure = endpointClosure
      self.requestClosure = requestClosure
      self.stubClosure = stubClosure
      self.session = session
      self.plugins = plugins
      self.trackInflights = trackInflights
      self.callbackQueue = callbackQueue
   }
}
  • MoyaProviderType
1
2
3
4
5
public protocol MoyaProviderType: AnyObject {

    associatedtype Target: TargetType
    func request(_ target: Target, callbackQueue: DispatchQueue?, progress: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> Cancellable
}

可以看出MoyaProvider是支持MoyaProviderType协议的的,主要作用是起到一个管理者的作用,负责串联各个模块。

  • defaultEndpointMapping
1
2
3
4
5
6
7
8
9
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
    )
}

Target不做任何处理,直接返回Endpoint.

  • defaultRequestMapping
1
2
3
4
5
6
7
8
9
10
11
12
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)))
    }
}

defaultRequestMapping主要作用是将Endpoint转为RequestResultClosure

  • StubBehavior
1
2
3
4
5
6
7
8
9
10
11
public enum StubBehavior {

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

 /// 立即使用Stub返回数据
 case immediate

 /// 一段时间间隔后使用Stub返回的数据.
 case delayed(seconds: TimeInterval)
}
1
2
3
4
final class func neverStub(_: Target) -> Moya.StubBehavior {
    /// 默认为不使用Stub返回数据
  	return .never
}
  • defaultAlamofireSession
1
2
3
4
5
6
final class func defaultAlamofireSession() -> Session {
    let configuration = URLSessionConfiguration.default
    configuration.headers = .default

    return Session(configuration: configuration, startRequestsImmediately: false)
}

默认使用 URLSessionConfiguration.default 来生成 SessionstartRequestsImmediately设置为false ,如果不设置为 false , Alamofire 在创建 Request 后就会直接发起请求,在进行stub测试的情况下进行。但是在 Alamofire5 中这块逻辑已经做了调整,在创建 Request 时不会直接发起请求,只有在调用 .response 相关方法添加响应处理后才会发起请求。

PluginType

Moya提供了一个插件协议、机制,主要用于请求发送或者接收时调用的,而且可以建立自己的插件类来做一些额外的事情,比如Log、“菊花”加载等。这里也使用协议对具体的对象类型进行抽象, MoyaProvider 不需要知道具体的类型是什么,只需要实现 PluginType 协议即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public protocol PluginType {
    /// 发送请求前调用,可以对URLRequest进行修改
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest
  
    /// 发送请求前最后调用的方法
    func willSend(_ request: RequestType, target: TargetType)

	  /// 接收到响应结果时调用,会先调用该方法
    /// 再调用MoyaProvider调用自己的completionHandler
    func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType)
	  
	  /// 响应结果的预处理器
    func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>
}

PluginType请求前由 Alamofire 的 RequestInterceptor 协议来实现。每次发起请求时, MoyaProvider 都会生成一个 ` MoyaRequestInterceptor` ,其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
final class MoyaRequestInterceptor: RequestInterceptor {
    private let lock: NSRecursiveLock = NSRecursiveLock()

    var prepare: ((URLRequest) -> URLRequest)?
    private var internalWillSend: ((URLRequest) -> Void)?

    var willSend: ((URLRequest) -> Void)? {
        get {
            lock.lock(); defer { lock.unlock() }
            return internalWillSend
        }

        set {
            lock.lock(); defer { lock.unlock() }
            internalWillSend = newValue
        }
    }

    init(prepare: ((URLRequest) -> URLRequest)? = nil, willSend: ((URLRequest) -> Void)? = nil) {
        self.prepare = prepare
        self.willSend = willSend
    }
    
    func adapt(_ urlRequest: URLRequest, for session: Alamofire.Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        // 先调 prepare 对 urlRequest 进行处理,再调 willSend 。
		  let request = prepare?(urlRequest) ?? urlRequest
        willSend?(request)
        completion(.success(request))
    }
}

/// 初始化
private func interceptor(target: Target) -> MoyaRequestInterceptor {
    return MoyaRequestInterceptor(prepare: { [weak self] urlRequest in

        return self?.plugins.reduce(urlRequest) { $1.prepare($0, target: target) } ?? urlRequest
    })
}

Provider发送请求

官方文档里说明的Moya的基本使用步骤:

  1. 创建枚举,遵守TargetType协议,实现规定的属性。
  2. 初始化 provider = MoyaProvider<Myservice>()
  3. 调用provider.request,在闭包里处理请求结果。

请求前预处理

MoyaProvider 下面的接口来发起请求:

1
2
3
4
5
6
7
8
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)
}

返回的结果使用了Cancellable协议进行了封装,

1
2
3
4
5
6
public protocol Cancellable {
	  /// 判断请求是否已经取消
    var isCancelled: Bool { get }
	  /// 取消请求
    func cancel()
}

通过 Cancellable 的接口来进行相关调用,经过callbackqueue的处理后会调用下面的方法。

1
2
3
4
5
6
7
8
9
10
func requestNormal(_ target: Target, callbackQueue: DispatchQueue?, progress: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> Cancellable {
  
  let endpoint = self.endpoint(target)
  let stubBehavior = self.stubClosure(target)
  let cancellableToken = CancellableWrapper()
  let pluginsWithCompletion: Moya.Completion = { result in
      let processedResult = self.plugins.reduce(result) { $1.process($0, target: target) }
      completion(processedResult)
  }
}
  • 1.self.endpoint(target)target转换为endpoint
1
2
3
open func endpoint(_ token: Target) -> Endpoint {
    return endpointClosure(token)
}
  • 2.self.stubClosure(target)获取Target对应的Stub

  • 3.生成一个CancellableWrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
internal class CancellableWrapper: Cancellable {
    internal var innerCancellable: Cancellable = SimpleCancellable()

    var isCancelled: Bool { return innerCancellable.isCancelled }

    internal func cancel() {
        innerCancellable.cancel()
    }
}

internal class SimpleCancellable: Cancellable {
    var isCancelled = false
    func cancel() {
        isCancelled = true
    }
}

CancellableWrapper 使用一个SimpleCancellable来实现Cancellable 协议,如果进行stub测试,就直接使用SimpleCancellable,如果发起实际请求,就会生成对应的 Cancellable ,替换 SimpleCancellable

  • 4.使用 reduce调用插件的 process 方法对相应结果进行处理。

预处理后一些处理

请求前预处理完后,会通过trackInflights判断是否需要进行处理重复请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if trackInflights {
    lock.lock()
	  /// 通过Endpoint获取对应的请求回调
    var inflightCompletionBlocks = self.inflightRequests[endpoint]
    /// 将pluginsWithCompletion追加到inflightCompletionBlocks
    inflightCompletionBlocks?.append(pluginsWithCompletion)
    self.inflightRequests[endpoint] = inflightCompletionBlocks
    lock.unlock()
    
    /// 判断inflightCompletionBlocks不为空时,表示有重复的请求在处理中,不需要发起新的请求,返回cancellableToken即可,否则设置对应的回调到inflightRequests中
    if inflightCompletionBlocks != nil {
        return cancellableToken
    } else {
        lock.lock()
        self.inflightRequests[endpoint] = [pluginsWithCompletion]
        lock.unlock()
    }
}

设置一个performNetworking闭包,这是执行请求的下一步,是在 endpoint → URLRequest 方法执行完成后的闭包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
let performNetworking = { (requestResult: Result<URLRequest, MoyaError>) in
    /// 判断请求是否被取消,如取消就不再发送请求
    /// 返回错误类型为cancel的错误提示数据
	  if cancellableToken.isCancelled {
        self.cancelCompletion(pluginsWithCompletion, target: target)
        return
    }
	  
    var request: URLRequest!
	  /// 判断请求结果是否为.success,如果不是则调用pluginsWithCompletion 处理对应的error 
    switch requestResult {
    case .success(let urlRequest):
        request = urlRequest
    case .failure(let error):
        pluginsWithCompletion(.failure(error))
        return
    }
	  /// 定义返回结果闭包,根据trackInflights调用不同的closure
    let networkCompletion: Moya.Completion = { result in
      if self.trackInflights {
        self.inflightRequests[endpoint]?.forEach { $0(result) }

        self.lock.lock()
        self.inflightRequests.removeValue(forKey: endpoint)
        self.lock.unlock()
      } else {
        /// 通过闭包通知所有插件,返回结果
        pluginsWithCompletion(result)
      }
    }
	  /// 通过调用performRequest请求,替换cancellableToken中的innerCancellable,将所有参数继续传递
    cancellableToken.innerCancellable = self.performRequest(target, request: request, callbackQueue: callbackQueue, progress: progress, completion: networkCompletion, endpoint: endpoint, stubBehavior: stubBehavior)
}

/// 通过requestClosure将Endpoint转换为URLRequest,转换完成后则调用 performNetworking
requestClosure(endpoint, performNetworking)
return cancellableToken

发起请求

performRequest根据是否进行stub测试调用不同的方法,根据 switch stubRequest 和 sendRequest 来分别执行对应的请求方式。

stubRequest

在进行stub测试时不会发起真正的网络请求,而是通过Endpoint 获取对应的假数据进行回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
open func stubRequest(_ target: Target, request: URLRequest, callbackQueue: DispatchQueue?, completion: @escaping Moya.Completion, endpoint: Endpoint, stubBehavior: Moya.StubBehavior) -> CancellableToken {
    let callbackQueue = callbackQueue ?? self.callbackQueue
    /// 初始化一个CancellableToken,由于没有发起真正的请求,所以CancellableToken 没有包含对应的 Request
    let cancellableToken = CancellableToken { }
    /// 调用插件的相关方法
    let preparedRequest = notifyPluginsOfImpendingStub(for: request, target: target)
    let plugins = self.plugins
	  /// 通过createStubFunction进行验证由Endpoint的 sampleResponseClosure来获取对应的假数据,同时也会调用插件的对应方法
    let stub: () -> Void = createStubFunction(cancellableToken, forTarget: target, withCompletion: completion, endpoint: endpoint, plugins: plugins, request: preparedRequest)
    switch stubBehavior {
    case .immediate:
        switch callbackQueue {
        case .none:
            stub()
        case .some(let callbackQueue):
            callbackQueue.async(execute: stub)
        }
    case .delayed(let delay):
        let killTimeOffset = Int64(CDouble(delay) * CDouble(NSEC_PER_SEC))
        let killTime = DispatchTime.now() + Double(killTimeOffset) / Double(NSEC_PER_SEC)
        (callbackQueue ?? DispatchQueue.main).asyncAfter(deadline: killTime) {
            stub()
        }
    case .never:
        fatalError("Method called to stub request when stubbing is disabled.")
    }

    return cancellableToken
}

sendRequest

1
2
3
4
5
6
7
8
9
10
11
func sendRequest(_ target: Target, request: URLRequest, callbackQueue: DispatchQueue?, progress: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> CancellableToken {
    /// 通过target生成一个MoyaRequestInterceptor来调用插件对应的方法
    let interceptor = self.interceptor(target: target)
    let initialRequest = session.request(request, interceptor: interceptor)
    setup(interceptor: interceptor, with: target, and: initialRequest)

    let validationCodes = target.validationType.statusCodes
    let alamoRequest = validationCodes.isEmpty ? initialRequest : initialRequest.validate(statusCode: validationCodes)
    /// 调用Alamofire的方法发送网络请求
    return sendAlamofireRequest(alamoRequest, target: target, callbackQueue: callbackQueue, progress: progress, completion: completion)
}

sendAlamofireRequest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
func sendAlamofireRequest<T>(_ alamoRequest: T, target: Target, callbackQueue: DispatchQueue?, progress progressCompletion: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> CancellableToken where T: Requestable, T: Request {
    /// 获取插件
    let plugins = self.plugins
    var progressAlamoRequest = alamoRequest
    /// 生成对应的progressClosure
    let progressClosure: (Progress) -> Void = { progress in
        let sendProgress: () -> Void = {
            progressCompletion?(ProgressResponse(progress: progress))
        }

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

    /// 判断progressCompletion不为空开始发送网络请求
    if progressCompletion != nil {
        switch progressAlamoRequest {
        case let downloadRequest as DownloadRequest:
            if let downloadRequest = downloadRequest.downloadProgress(closure: progressClosure) as? T {
                progressAlamoRequest = downloadRequest
            }
        case let uploadRequest as UploadRequest:
            if let uploadRequest = uploadRequest.uploadProgress(closure: progressClosure) as? T {
                progressAlamoRequest = uploadRequest
            }
        case let dataRequest as DataRequest:
            if let dataRequest = dataRequest.downloadProgress(closure: progressClosure) as? T {
                progressAlamoRequest = dataRequest
            }
        default: break
        }
    }
	  /// 生成 completionHandler ,这一步的目的是为了将 Alamofire 的 response 转换为 Moya 所需要的格式,调用插件的 didReceive 方法
    let completionHandler: RequestableCompletion = { response, request, data, error in
        let result = convertResponseToResult(response, request: request, data: data, error: error)
        plugins.forEach { $0.didReceive(result, target: target) }
        if let progressCompletion = progressCompletion {
            let value = try? result.get()
            switch progressAlamoRequest {
            case let downloadRequest as DownloadRequest:
                progressCompletion(ProgressResponse(progress: downloadRequest.downloadProgress, response: value))
            case let uploadRequest as UploadRequest:
                progressCompletion(ProgressResponse(progress: uploadRequest.uploadProgress, response: value))
            case let dataRequest as DataRequest:
                progressCompletion(ProgressResponse(progress: dataRequest.downloadProgress, response: value))
            default:
                progressCompletion(ProgressResponse(response: value))
            }
        }
        completion(result)
    }

    progressAlamoRequest = progressAlamoRequest.response(callbackQueue: callbackQueue, completionHandler: completionHandler)

    progressAlamoRequest.resume()

    return CancellableToken(request: progressAlamoRequest)
}

上述请求流程基本是Moya完整的网络请求流程,可以看到Moya是在Alamofire基础上提供了一层封装,而且是面向协议的一层封装,简单易用,同时也提供了足够灵活的插件和入口给开发者调用。

总结

Plugins 插件

Moya提供比较完整的插件功能:

  • AccessTokenPlugin 管理AccessToken的插件
  • CredentialsPlugin 管理认证的插件
  • NetworkActivityPlugin 管理网络状态的插件
  • NetworkLoggerPlugin 管理网络log的插件

协议编程

Moya中使用比较多的协议来接口进行抽象,比如TargetTypePluginTypeCancellable等,使用协议可以更容易对接口惊醒扩展和隐藏具体的类型,同时也可以通过extension来提供默认实现。

1
2
3
4
5
6
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 }
}

枚举enum使用

Moya中使用enum来实现不同的逻辑调用,这样更加安全易用。

1
2
3
4
5
6
7
8
9
10
11
public enum EndpointSampleResponse {

    /// The network returned a response, including status code and data.
    case networkResponse(Int, Data)

    /// The network returned response which can be fully customized.
    case response(HTTPURLResponse, Data)

    /// The network failed to send the request, or failed to retrieve a response (eg a timeout).
    case networkError(NSError)
}

Alamofire桥接使用

Moya是在Alamofire基础上封装的网络请求框架,很多基本的基础类型都是Alamofire提供,Moya通过使用类型桥接隐藏Alamofire

1
2
3
4
5
6
7
8
public typealias Session = Alamofire.Session
public typealias Method = Alamofire.HTTPMethod
public typealias ParameterEncoding = Alamofire.ParameterEncoding
public typealias JSONEncoding = Alamofire.JSONEncoding
public typealias URLEncoding = Alamofire.URLEncoding
public typealias RequestMultipartFormData = Alamofire.MultipartFormData
public typealias DownloadDestination = Alamofire.DownloadRequest.Destination
public typealias RequestInterceptor = Alamofire.RequestIntercepto

Encodable

MoyaTask支持Encodable进行编码,在 Endpoint 生成对应的 URLRequest 时会通过 Encodable 参数来设置 httpBody

1
2
3
extension Endpoint {
  case let .requestJSONEncodable(encodable):
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
internal extension URLRequest {
    mutating func encoded(encodable: Encodable, encoder: JSONEncoder = JSONEncoder()) throws -> URLRequest {
        do {
            let encodable = AnyEncodable(encodable)
            httpBody = try encoder.encode(encodable)

            let contentTypeHeaderName = "Content-Type"
            if value(forHTTPHeaderField: contentTypeHeaderName) == nil {
                setValue("application/json", forHTTPHeaderField: contentTypeHeaderName)
            }

            return self
        } catch {
            throw MoyaError.encodableMapping(error)
        }
    }
}

这里需要声明方法为 mutating ,因为方法会修改属性。可以看到方法会通过 encodable 参数生成一个 AnyEncodable struct

1
2
3
4
5
6
7
8
9
10
11
12
struct AnyEncodable: Encodable {

    private let encodable: Encodable

    public init(_ encodable: Encodable) {
        self.encodable = encodable
    }

    func encode(to encoder: Encoder) throws {
        try encodable.encode(to: encoder)
    }
}

通过 AnyEncodable 可以把传进行来的协议参数转换成具体的值参数,而这个参数是遵循 Encodable 协议的。这是在 Swift 上进行类型擦除的一种方式,把具体的类型隐藏起来。为什么要进行类型擦除呢?因为 JSONEncoderencode 方法只接收具体的类型参数,不接受协议参数:

1
open func encode<T>(_ value: T) throws -> Data where T : Encodable 

MultiTarget

MoyaMoyaProvider使用时需要指定TargetType的类型,而 App 中可能会根据 baseURL 分成多个 TargetType ,这样导致我们需要根据不同的 TargetType 提供不同的 MoyaProviderMoya提供了MultiTarget来解决只用一个MoyaProvider来支持所有的TargetType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
enum MultiTarget: TargetType {
    case target(TargetType)
    public init(_ target: TargetType) {
        self = MultiTarget.target(target)
    }
    public var path: String {
        return target.path
    }
    public var baseURL: URL {
        return target.baseURL
    }
    public var method: Moya.Method {
        return target.method
    }
    public var sampleData: Data {
        return target.sampleData
    }
    public var task: Task {
        return target.task
    }
    public var validationType: ValidationType {
        return target.validationType
    }
    public var headers: [String: String]? {
        return target.headers
    }
    public var target: TargetType {
        switch self {
        case .target(let target): return target
        }
    }
}

定义一个使 multiple targetprovider:

1
let provider = MoyaProvider<MultiTarget>()

RxSwift

Moya可以跟RxSwift响应式框架进行交互.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extension Reactive where Base: MoyaProviderType {
    func request(_ token: Base.Target, callbackQueue: DispatchQueue? = nil) -> Single<Response> {
        return Single.create { [weak base] single in
            let cancellableToken = base?.request(token, callbackQueue: callbackQueue, progress: nil) { result in
                switch result {
                case let .success(response):
                    single(.success(response))
                case let .failure(error):
                    single(.error(error))
                }
            }
            return Disposables.create {
                cancellableToken?.cancel()
            }
        }
    }
}

普通请求返回的是 SingleSingleObservable 的另外一个版本,不可以发出多个元素,只能发出一个元素或者一个error事件,这和网络请求的流程一致,每个请求只能返回一个响应结果。

带有进度的请求如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func requestWithProgress(_ token: Base.Target, callbackQueue: DispatchQueue? = nil) -> Observable<ProgressResponse> {
    let progressBlock: (AnyObserver) -> (ProgressResponse) -> Void = { observer in
        return { progress in
            observer.onNext(progress)
        }
    }

    let response: Observable<ProgressResponse> = Observable.create { [weak base] observer in
        let cancellableToken = base?.request(token, callbackQueue: callbackQueue, progress: progressBlock(observer)) { result in
            switch result {
            case .success:
                observer.onCompleted()
            case let .failure(error):
                observer.onError(error)
            }
        }

        return Disposables.create {
            cancellableToken?.cancel()
        }
    }

    // Accumulate all progress and combine them when the result comes
    return response.scan(ProgressResponse()) { last, progress in
        let progressObject = progress.progressObject ?? last.progressObject
        let response = progress.response ?? last.response
        return ProgressResponse(progress: progressObject, response: response)
    }
}

这里使用Observable,因为在请求过程中会调用onNext更新进度。使用scan操作符判断是否需要使用之前的response

ReactiveCocoa

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func request(_ token: Base.Target, callbackQueue: DispatchQueue? = nil) -> SignalProducer<Response, MoyaError> {
    return SignalProducer { [weak base] observer, lifetime in
        let cancellableToken = base?.request(token, callbackQueue: callbackQueue, progress: nil) { result in
            switch result {
            case let .success(response):
                observer.send(value: response)
                observer.sendCompleted()
            case let .failure(error):
                observer.send(error: error)
            }
        }
        lifetime.observeEnded {
            cancellableToken?.cancel()
        }
    }
}

由于ReactiveCocoa没有类似RxSwift那样的Single ,所以对于普通请求,会在 send(value: Value)之后立即调用sendComplete()

其他

Moya也为我们提供了很多Observable的扩展,让我们能更轻松的处理MoyaResponse,常用的如下:

  • filter(statusCodes:) 过滤response状态码
  • filterSuccessfulStatusCodes() 过滤状态码为请求成功的
  • mapJSON()将请求response转化为JSON格式
  • mapString() 将请求response转化为String格式
1
2
3
4
5
6
7
8
9
10
11
12
func filter<R: RangeExpression>(statusCodes: R) throws -> Response where R.Bound == Int {
    guard statusCodes.contains(statusCode) else {
        throw MoyaError.statusCode(self)
    }
    return self
}
func filter(statusCode: Int) throws -> Response {
    return try filter(statusCodes: statusCode...statusCode)
}
func filterSuccessfulStatusCodes() throws -> Response {
    return try filter(statusCodes: 200...299)
}
1
2
3
4
5
6
7
8
9
10
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)
    }
}

开源社区

Extensions

应用

  • Drrrible - Dribbble for iOS using ReactorKit
  • Eidolon - The Artsy Auction Kiosk App
  • Insights for Instagram - A simple iOS Instagram’s media insights App written in Swift 4
  • Papr - An Unsplash app for iOS.
  • SwiftHub - Reactive Github iOS client written in RxSwift and MVVM.
  • GitTime - GitHub iOS client using ReactorKit and Moya.
本文由作者按照 CC BY 4.0 进行授权

缺失数字

分糖果