第三方框架閱讀的文章千千萬,寫下文章的目的一個是爲了督促本身完整的學習,另外一個目的是輸出倒逼本身思考git
Moya 是在 Alamofire 基礎上的一個網絡抽象層。簡單的介紹能夠移步對應的 Github 地址github
由於庫的總體結構是根據 POP 面向協議編程的思想,因此先來講幾句 POP。編程
所謂協議,就是一組屬性和/或方法的定義,而若是某個具體類型想要遵照一個協議,那它須要實現這個協議所定義的全部這些內容。協議實際上作的事情不過是「關於實現的約定」。swift
在喵神的博客中,由兩片關於協議的應用,貼出來原文的地址api
最基礎使用的協議,用 class / struct/enum 實現均可以。閉包
利用枚舉的特性就能夠比較清晰的管理,或者也能夠class/struct + enum 的形式管理app
enum NormalRequest {
case post(String)
}
複製代碼
validationType 屬性,是對 Alamofire 返回的結果根據 statusCode 驗證,也就是 Alamofire 自動化驗證功能框架
extension NormalRequest: TargetType { ...
var validationType: ValidationType {
return .successAndRedirectCodes
}
}
複製代碼
MoyaProvider
就是 Moya 最頂層的請求頭。簡單的設置了 target 以後,初始化一個對應 target 的 provider,不須要額外的參數
let provider = MoyaProvider<NormalRequest>()
複製代碼
可是直接從 init
方法開始看,會發現能夠設置 7 個參數,並且都給了默認實現
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+Defaults 文件裏,一共只有三個方法,都是 MoyaProvider 的擴展裏 。分別對應的就是 init 方法中的 endpointClosure ,requestClosure,manager 三者的默認實現方法。
這裏是從 Target → 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 對象
open func adding(newHTTPHeaderFields: [String: String]) -> Endpoint { ... }
open func replacing(task: Task) -> Endpoint { ... }
複製代碼
從 Endpoint → URLRequest ,根據 Endpoint 的數據,生成一個 URLRequest
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)))
}
}
複製代碼
這裏的默認實現,是一個有着基本配置的 Alamofire.Session
對象。
對於 startRequestsImmediately ,文檔中有這麼解釋
There is only one particular thing: since construct an Alamofire.Request in AF will fire the request immediately by default, even when "stubbing" the requests for unit testing. Therefore in Moya, startRequestsImmediately is set to false by default.
也就是以前每次構造出一個 request以後,默認狀況下就是馬上執行。因此在 Moya 中設置爲 false
final class func defaultAlamofireManager() -> Manager {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = Manager.defaultHTTPHeaders
let manager = Manager(configuration: configuration)
manager.startRequestsImmediately = false
return manager
}
複製代碼
插件,Moya 一個十分特點的特性。對應的協議是 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>
複製代碼
各個方法調用的時機
在 target 中的 sampleData,能夠是本身提供測試數據,用來不經過實際網絡請求來模擬返回的數據結果。在 provider 初始化中,stubClosure 閉包是用來設置是否須要模擬測試
.never
.immediate
.delayed(seconds: TimeInterval)
在 Moya 中對於數據測試還有更多的選擇 sampleResponseClosure
.networkResponse(Int, Data)
.response(HTTPURLResponse, Data)
.networkError(NSError)
模擬各類錯誤那麼 stub 分別能夠在三個地方設置本身的配置
// 1. 設置測試數據
public var sampleData: Data {
switch self {
case .userRepositories(let name):
return "[{\"name\": \"Repo Name\"}]".data(using: String.Encoding.utf8)!
}
}
// 是否須要,且如何響應測試數據
let stubbingProvider = MoyaProvider<GitHub>(stubClosure: MoyaProvider.immediatelyStub)
// 以怎麼樣的響應數據返回
let customEndpointClosure = { (target: APIService) -> Endpoint in
return Endpoint(url: URL(target: target).absoluteString,
sampleResponseClosure: { .networkResponse(401 , /* data relevant to the auth error */) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers)
}
let stubbingProvider = MoyaProvider<GitHub>(endpointClosure: customEndpointClosure, stubClosure: MoyaProvider.immediatelyStub)
複製代碼
provider.request(.post("")){(result) in
swtich result {
case let .success(response):
break
case let .fail(_):
break
}
}
複製代碼
在 Moya 中 request
方法是一個統一的請求入口。只須要在方法中配置須要的參數,包括須要對應生成的請求地址,請求參數等經過枚舉類型,十分清晰的分類和管理。利用 .
語法生成對應的枚舉,而後依次生成 endpoint,URLRequest等。
方法中就能夠發現,在這個方法裏,還能夠再次修改 callbackQueue
@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)
}
複製代碼
這個方法,作了一些生成請求時的通用處理。
在 init 方法中,不只能夠傳入對應的參數,還有一個目的是持有這些屬性。
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()
複製代碼
由於 init 時參數都是默認實現,因此第一句代碼生成 endpoint 用的是 Moya 庫裏的實現方法。
這裏會調用的是 defaultEndpointMapping 方法,返回 一個 Endpoint 對象
若是初始化時傳入了本身定義的 endpoint 映射閉包,那麼就會以自定義的方法執行
open func endpoint(_ token: Target) -> Endpoint {
return endpointClosure(token)
}
複製代碼
cancellableToken
是遵循 Cancellable
協議的類 CancellableWrapper
的對象。
這個協議就只有兩行代碼,是否已經取消的屬性和取消請求的方法。並且這個方法在類中的實現就是將屬性的值改成 true
/// A Boolean value stating whether a request is cancelled.
var isCancelled: Bool { get }
/// Cancels the represented request.
func cancel()
複製代碼
這裏就是插件 process 的執行位置,對每一個插件都執行一次。插件實現的 process 的方法對 result 的全部改動,最後都經過 completion 返回。
let pluginsWithCompletion: Moya.Completion = { result in
let processedResult = self.plugins.reduce(result) { $1.process($0, target: target) }
completion(processedResult)
}
複製代碼
字面上理解,就是真正執行請求的下一步了。這個閉包,是在 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
}
// Allow plugins to modify request
// 插件執行 prepare
let preparedRequest = self.plugins.reduce(request) { $1.prepare($0, target: target) }
// 定義返回結果閉包,這裏返回的是請求返回的數據映射成了 Result
let networkCompletion: Moya.Completion = { result in
if self.trackInflights {
....
} else {
// 使用上面的閉包,通知全部插件,且返回結果
pluginsWithCompletion(result)
}
}
// 這一步就是執行請求的下一步了,將全部參數繼續傳遞
cancellableToken.innerCancellable = self.performRequest(target, request: preparedRequest, callbackQueue: callbackQueue, progress: progress, completion: networkCompletion, endpoint: endpoint, stubBehavior: stubBehavior)
}
複製代碼
接下去的就是將上面定義好的兩個閉包,傳入到 requestClosure 閉包中
endpoint 生成 URLRequest,而後請求完成以後執行 performNetworking 閉包內的代碼
requestClosure(endpoint, performNetworking)
複製代碼
在這個方法中,有兩段根據 trackInflights 屬性的代碼。
查閱一些資料以後,發現不多說起這個屬性的解釋,根據代碼邏輯能夠看出來,這是是否對重複請求狀況的處理。其中有一個解釋是:是否要跟蹤重複網絡請求
在 #229 中,提到這個特性是指 Moya 一開始跟蹤 API 請求,而後作了防止重複請求的處理。問題提出,在有些時間咱們是須要重複請求一個 API 的,因此當時的作法是,#232 刪除了 防止重複請求的代碼。#477 中,纔有瞭如今的這一版,對重複 API 的請求利用字典管理。
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 {
// 若是不存在 key 爲 endpoint 的值,則初始化一個
objc_sync_enter(self)
self.inflightRequests[endpoint] = [pluginsWithCompletion]
objc_sync_exit(self)
}
}
....
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)
}
}
複製代碼
一個請求在 init 的時候將 trackInflights 設置爲 true,那麼在 Moya 中就會存儲這個請求的 endpoint。在返回數據的時候,若是須要跟蹤了重複請求,那麼就將一次實際發送請求返回的數據,屢次返回。
cancellableToken.innerCancellable =
self.performRequest(target,
request: preparedRequest, // 插件執行 prepare 閉包返回 request
callbackQueue: callbackQueue,
progress: progress,
completion: networkCompletion, // 網絡請求成功以後,將結果返回的閉包
endpoint: endpoint,
stubBehavior: stubBehavior)
複製代碼
這個方法的內部實現,根據 switch stubBehavior 和 endpoint.task 來分別執行對應的請求方式。這裏只貼除了最簡單的一個
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)
....
}
default:
return self.stubRequest...
}
複製代碼
通常請求的實現。在這一層,就是和 Alamofire 產生聯繫的一層。由於咱們在以前都已經生成了 urlRequest 。因此都是對應生成 DataRequest,DownloadRequest,UploadRequest
func sendRequest(_ target: Target, request: URLRequest, callbackQueue: DispatchQueue?, progress: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> CancellableToken {
let initialRequest = manager.request(request as URLRequestConvertible)
let validationCodes = target.validationType.statusCodes
let alamoRequest = validationCodes.isEmpty ? initialRequest : initialRequest.validate(statusCode: validationCodes)
return sendAlamofireRequest(alamoRequest, target: target, callbackQueue: callbackQueue, progress: progress, completion: completion)
}
複製代碼