Moya 源码分析

Moya 源码分析

面向协议编程

OOP 带来的三个问题

  • 横切关注点
  • 菱形缺陷
  • 动态派发的安全性

面向协议参考文章

Moya简单介绍和使用

Moya是一个基于Alamofire开发的,轻量级的Swift网络层。Moya的可扩展性非常强,可以方便的RXSwift等函数式框架结合。

简单使用

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
private func JSONResponseDataFormatter(_ data: Data) -> Data {
do {
let dataAsJSON = try JSONSerialization.jsonObject(with: data)
let prettyData = try JSONSerialization.data(withJSONObject: dataAsJSON, options: .prettyPrinted)
return prettyData
} catch {
return data // fallback to original data if it can't be serialized.
}
}
/// 创建请求管理者
let gitHubProvider = MoyaProvider<GitHub>(plugins: [NetworkLoggerPlugin(verbose: true, responseDataFormatter: JSONResponseDataFormatter)])
/// 创建请求target
public enum GitHub {
case zen
case userProfile(String)
case userRepositories(String)
}
extension GitHub: TargetType {
public var baseURL: URL { return URL(string: "https://api.github.com")! }
public var path: String {
switch self {
case .zen:
return "/zen"
case .userProfile(let name):
return "/users/\(name.urlEscaped)"
case .userRepositories(let name):
return "/users/\(name.urlEscaped)/repos"
}
}
public var method: Moya.Method {
return .get
}
public var task: Task {
switch self {
case .userRepositories:
return .requestParameters(parameters: ["sort": "pushed"], encoding: URLEncoding.default)
default:
return .requestPlain
}
}
public var validationType: ValidationType {
switch self {
case .zen:
return .successCodes
default:
return .none
}
}
public var sampleData: Data {
switch self {
case .zen:
return "Half measures are as bad as nothing at all.".data(using: String.Encoding.utf8)!
case .userProfile(let name):
return "{\"login\": \"\(name)\", \"id\": 100}".data(using: String.Encoding.utf8)!
case .userRepositories(let name):
return "[{\"name\": \"\(name)\"}]".data(using: String.Encoding.utf8)!
}
}
public var headers: [String: String]? {
return nil
}
}
/// 发起请求
gitHubProvider.request(.userRepositories(username)) { result in
do {
let response = try result.dematerialize()
let value = try response.mapNSArray()
self.repos = value
self.tableView.reloadData()
} catch {
let printableError = error as CustomStringConvertible
self.showAlert("GitHub Fetch", message: printableError.description)
}
}

框架组成

Moya的项目主要由八个部分组成。

request

request部分主要包含请求相关的代码。定义了TargetType协议,通过targetType 来定义一个请求

  • TargetType: 请求相关信息
  • Endpoint: Moya进行请求的最终数据结构
  • Task: 定义了请求的发起方式,请求参数设置等
  • Cancellable: 请求取消用

Provider

Provider中包含了最核心的代码,主要负责组装 TargetType 和 Endpoint,并发起请求和转换请求结果为Response

  • MoyaProvider: 主要的核心代码
  • MoyaProvider+Defaults: 提供一些内部需要的默认实现
  • MoyaProvider+Internal: 提供了内部使用的方法

response

响应结果相关方法,以及字典,JSON等相关转换方法

  • MoyaError: 封装了相关错误
  • Response: 核心响应体类

Plugins

插件相关。

  • Plugin: 插件核心定义,用于扩展其他插件
  • AccessTokenPlugin: 授权插件
  • NetworkActivityPlugin: 显示网络活动插件(转圈小菊花靠这个来显示)
  • NetworkLoggerPlugin: 像控制台输出日志的插件
  • CredentialsPlugin: https证书交换插件

Alamofire

Alamofire 的封装层

  • MultipartFormData: 表单数据的封装
  • Moya+Alamofire: Alamofire类型桥接

utils

工具类

Extension

其他框架的扩展支持,目前主要就是ReativeCocoa和RxSwift的扩展

test

单元测试相关

Moya Structure

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

TargetType协议定义了调用方需要实现哪些属性,来提供一个请求所需要的值。

Provider

Providers是Moya中的核心,Moya中所有的API请求都是通过Provider来发起的。因此大多数时候,你的代码请求像这样:

1
2
3
4
5
6
7
8
9
10
11
gitHubProvider.request(.userRepositories(username)) { result in
do {
let response = try result.dematerialize()
let value = try response.mapNSArray()
self.repos = value
self.tableView.reloadData()
} catch {
let printableError = error as CustomStringConvertible
self.showAlert("GitHub Fetch", message: printableError.description)
}
}

Provider 真正在做的事情可以用一个流来表示: Target -> Endpoint -> Request .这个过程中 Provider会一步步来生成一个NSURLRequest来交给Alamofire发起请求.

下面我们从Provider的构造函数开始来看一看这个类的组成

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

首先我们可以看到三个闭包,这三个闭包我们放到最后说。

  • 首先一个callbackQueue,如字面意思,回调的线程。
  • manager: manager是最终真正用来请求的类,是由Alamofire桥接过来的。Moya只对Alamofire的类型进行了简单的桥接,这样做的好处是,如果调用方不使用Alamofire的话,我们可以快速替换网络请求库,moya提供了一个默认的Manager实现
1
2
3
4
5
6
7
8
public final class func defaultAlamofireManager() -> Manager {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = Manager.defaultHTTPHeaders
let manager = Manager(configuration: configuration)
manager.startRequestsImmediately = false
return manager
}
  • plugins: 需要使用到的插件,这是一个非常方便的功能,能保证在不修改源码的情况下,来扩展其他功能。而且能够保证Provider的职责单一,满足开闭原则。
  • trackInFlights: 是否要追踪请求。这个默认是不追踪的。如果传入true的话,

接下来是最核心的三个闭包

  • EndpointClosure: 这个closure负责从TargetType转换成Endpoint, EndPoint 是最终发起请求的一个数据格式,保存了下面数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
open class Endpoint {
public typealias SampleResponseClosure = () -> EndpointSampleResponse
/// A string representation of the URL for the request.
open let url: String
/// A closure responsible for returning an `EndpointSampleResponse`.
open let sampleResponseClosure: SampleResponseClosure
/// The HTTP method for the request.
open let method: Moya.Method
/// The `Task` for the request.
open let task: Task
/// The HTTP header fields for the request.
open let httpHeaderFields: [String: String]?

从初始化方法中我们可以看到,Moya 对于 EndpointClosure 提供了默认实现,我们可以在MoyaProvider+Defaults.swift 中找到它

1
2
3
4
5
6
7
8
9
public 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:负责把Endpoint 转换成 NSURLRequest,同样的我们可以在 MoyaProvider+Defaults.swift 中找到它
1
2
3
4
5
6
7
8
9
10
11
12
public 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)))
}
}
  • stubClosure: 这个闭包会返回一个枚举 StubBehavior , 默认会返回never,如果你需要假数据测试的话可以重写这里。
1
2
3
4
5
6
7
8
9
10
11
public enum StubBehavior {
/// 不进行stub
case never
/// 立即返回假数据(即TargetType中的sampleData)
case immediate
/// 延迟x秒后返回
case delayed(seconds: TimeInterval)
}

Moya采取了最简单粗暴的方式来返回假数据.如果是never 则进行真正的请求。否则直接进入假数据部分.

MoyaProvider+Internal.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
}
}

Manager和Alamofire

Moya在 Moya+Alamofire.swift 文件中对 Alamofire进行了简单的包装,这一层包装隔离了Alamofire的代码,如果需要更换请求框架的话,可以进行简单的修改来替换,而不必修改外部调用。

如果你想创建自己的Manager的话,可以在Provider初始化的时候传入自己的Manager

Plugin

Moya提供了非常好用的插件机制,通过插件你可以在不修改源码的基础上,充分的扩展Moya的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
///Plugin.swift
public protocol PluginType {
/// Called to modify a request before sending.
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest
/// Called immediately before a request is sent over the network (or stubbed).
func willSend(_ request: RequestType, target: TargetType)
/// Called after a response has been received, but before the MoyaProvider has invoked its completion handler.
func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType)
/// Called to modify a result before completion.
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 }
}

调用方可以通过实现上面协议,来创建各种插件处理不同事情,从而保证Provider的单一职责。

  • func prepare(_ request: URLRequest, target: TargetType) -> URLRequest 方法会在Endpoint生成Request之前调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
////MoyaProvider+Internal.swift
func requestNormal(_ target: Target, callbackQueue: DispatchQueue?, progress: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> Cancellable {
.......
// Allow plugins to modify request
let preparedRequest = self.plugins.reduce(request) { $1.prepare($0, target: target) }
.......
return cancellableToken
}
  • func willSend(_ request: RequestType, target: TargetType) 方法会在发送Alamofire请求之前调用
1
2
3
4
func sendAlamofireRequest<T>(_ alamoRequest: T, target: Target, callbackQueue: DispatchQueue?, progress progressCompletion: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> CancellableToken where T: Requestable, T: Request {
// Give plugins the chance to alter the outgoing request
let plugins = self.plugins
plugins.forEach { $0.willSend(alamoRequest, target: target) }
  • func didReceive(_ result: Result, target: TargetType) 收到Response的时候调用

  • func process(_ result: Result, target: TargetType) -> Result 请求完成后调用。

Extension

Moya 默认提供了 ReactiveCocoa的扩展ReactiveMoya 和 RxSwift扩展RxMoya。 这里简单看一下RxMoya的实现。

首先我们可以看到三个关于RxMoya的文件

  • MoyaProvider+Rx.swift 主要的扩展文件
  • Observable+Response.swift Response部分的扩展
  • Single+Response.swift Single Response部分的扩展
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
62
extension MoyaProvider: ReactiveCompatible {}
// 对Rx中 Base 为 MoyaProviderType 进行扩展
public extension Reactive where Base: MoyaProviderType {
/// Designated request-making method.
///
/// - Parameters:
/// - token: Entity, which provides specifications necessary for a `MoyaProvider`.
/// - callbackQueue: Callback queue. If nil - queue from provider initializer will be used.
/// - Returns: Single response object.
///Single: 只会发送一个值,或者发送一次error
public 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()
}
}
}
/// Designated request-making method with progress.
public 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)
}
}
}