關於Moya
Moya是一個網路抽象層,它在底層將Alamofire進行封裝,對外提供更簡潔的介面供開發者呼叫。在以往的Objective-C中,大部分開發者會使用AFNetwork進行網路請求,當業務複雜一些時,會對AFNetwork進行二次封裝,編寫一個適用於自己專案的網路抽象層。在Objective-C中,有著名的YTKNetwork,它將AFNetworking封裝成抽象父類,然後根據每一種不同的網路請求,都編寫不同的子類,子類繼承父類,來實現請求業務。Moya在專案層次中的地位,有點類似於YTKNetwork。可以看下圖對比
但是如果單純把Moya等同於swift版的YTKNetwork,那就是比較錯誤的想法了。Moya的設計思路和YTKNetwork差距非常大。上面我在介紹YTKNetwork時在強調子類和父類,繼承,是因為YTKNetwork是比較經典的利用OOP思想(物件導向)設計的產物。基於swift的Moya雖然也有使用到繼承,但是它的整體上是以POP思想(Protocol Oriented Programming,面向協議程式設計)為主導的。
面向協議程式設計(POP)
在閱讀Moya原始碼之前,如果對POP有一定了解,那麼理解其Moya會事半功倍的效果。在Objective-C也有協議,一般是讓物件遵守協議,然後實現協議規定的方法,通過這種方式來對類實現擴充套件。POP其實就是把這種思路進一步強化。很多時候事物具備多樣化的特質,而這些特質是無法單純從一個類中繼承而來的。為了解決這個痛點,C++有了多繼承,即一個子類可以繼承多種父類,這些被繼承的父類之間不一定有關聯。但是這依然會有其他問題,比如子類繼承父類後,不一定需要用到所有的父類方法和屬性,等於子類擁有了一些毫無用處的屬性和方法。比如父類進行了修改,那麼很難避免影響到子類。C++的多繼承還會帶來菱形缺陷
,什麼是菱形缺陷
?本節的下方我會放兩個連結,方便大家查閱。而Swift則引入了面向協議程式設計,通過協議來規定事物的實現。通過遵守不同的協議,來對一個類或者結構體或者列舉進行定製,它只需要實現協議所規定的屬性或方法即可,有點類似於搭建積木,取每一塊有需求的模組,進行組合拼接,相對於OOP,其耦合性更低,也為程式碼的維護和擴充提供更多的可能性。關於POP思想大致是這樣,下面是王巍關於POP的兩篇文章,值得讀一番。
面向協議程式設計與 Cocoa 的邂逅 (上)
面向協議程式設計與 Cocoa 的邂逅 (下)
Moya的模組組成
由於Moya是使用POP來設計的一個網路抽象層,因此他整體的邏輯結構並沒有明顯的繼承關係。Moya的核心程式碼,可以分成以下幾個模組
Provider
provider是一個提供網路請求服務的提供者。通過一些初始化配置之後,在外部可以直接用provider來發起request。
Request
在使用Moya進行網路請求時,第一步需要進行配置,來生成一個Request。首先按照官方文件,建立一個列舉,遵守TargetType協議,並實現協議所規定的屬性。為什麼要建立列舉來遵守協議,而不像Objective-C那樣建立類來遵守協議呢?其實使用類或者結構體也是可以的,這裡猜測使用列舉的原因是因為swift的列舉功能比Objective-C強大許多,列舉結合switch語句,使得API管理起來比較方便。
Request的生成過程如下圖
我們根據上圖,結合程式碼來分析其Request的生成過程。
根據建立了一個遵守TargetType協議的名為Myservice的列舉,我們完成了如下幾個變數的設定。
baseURL
path
method
sampleData
task
headers
複製程式碼
提供了這些網路請求的“基本材料”之後,就可以進一步配置去生成所需要的請求。看上圖的第一個箭頭,通過了一個EndpointClosure生成了endPoint。endPoit是一個物件,把網路請求所需的一些屬性和方法進行了包裝,在EndPoint類中有如下屬性:
public typealias SampleResponseClosure = () -> EndpointSampleResponse
open let url: String
open let sampleResponseClosure: SampleResponseClosure
open let method: Moya.Method
open let task: Task
open let httpHeaderFields: [String: String]?
複製程式碼
可以很直觀地看出來,EndPoint這幾個屬性可以和上面通過TargetTpye配置的變數對應起來。那麼這個過程在程式碼中做了哪些事?
在MoyaProvider類裡,有如下宣告
/// Closure that defines the endpoints for the provider.
public typealias EndpointClosure = (Target) -> Endpoint<Target>
open let endpointClosure: EndpointClosure
複製程式碼
宣告瞭一個閉包,引數為Target,它是一個泛型,然後返回一個EndPoint。endPoint是一個類,它對請求的引數和動作進行了包裝,下面會對它進行詳細說明,先繼續看endpointClosure做了什麼。
endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping
複製程式碼
在MoyaProvider的初始化方法裡,呼叫其擴充套件的類方法defaultEndpointMapping
輸入Target作為引數,返回了一個endPoint物件。
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
)
}
複製程式碼
Target就是一開始進行配置的列舉,通過點語法取出Target的變數,完成endPoint的初始化。這裡可能對於url和sampleResponseClosure會感到一些疑惑。url初始化,可以進入URL+Moya.swift
檢視,它對NSURL類進行構造器的擴充套件,讓其具備根據Moya的TargetType來進行初始化的能力。
/// Initialize URL from Moya`s `TargetType`.
init<T: TargetType>(target: T) {
// When a TargetType`s path is empty, URL.appendingPathComponent may introduce trailing /, which may not be wanted in some cases
if target.path.isEmpty {
self = target.baseURL
} else {
self = target.baseURL.appendingPathComponent(target.path)
}
}
複製程式碼
sampleResponseClosure是一個和網路請求返回假資料相關的閉包,這裡可以先忽略,不影響對Moya生成Request過程的理解。
我們知道了MoyaProvider.defaultEndpointMapping可以返回endPoint物件後,重新看一遍這句
endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping
複製程式碼
使用@escaping把endpointClosure宣告為逃逸閉包,我們可以把
EndpointClosure = MoyaProvider.defaultEndpointMapping
複製程式碼
轉換為
(Target) -> Endpoint<Target> = func defaultEndpointMapping(for target: Target) -> Endpoint<Target>
複製程式碼
再進一步轉換,等號左邊的可以寫成一個常規的閉包表示式
{(Target)->Endpoint<Target> in
return Endpoint(
url: URL(target: target).absoluteString,
sampleResponseClosure: { .networkResponse(200, target.sampleData) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers
)
}
複製程式碼
即endpointClosure這個閉包,傳入了Target作為引數,該閉包可以返回一個endPoint物件,如何獲取到閉包返回的endPoint物件?MoyaProvider提供了這麼一個方法
/// Returns an `Endpoint` based on the token, method, and parameters by invoking the `endpointClosure`.
open func endpoint(_ token: Target) -> Endpoint<Target> {
return endpointClosure(token)
}
複製程式碼
以上就是關於TargetType通過endpointClosure轉化為endPoint的過程。
下一步就是把利用requestClosure,傳入endPoint,然後生成request。request生成過程和endPoint很相似。
在MoyaProvider中宣告:
/// Closure that decides if and what request should be performed
public typealias RequestResultClosure = (Result<URLRequest, MoyaError>) -> Void
open let requestClosure: RequestClosure
複製程式碼
然後在MoyaProvider的初始化方法裡有很相似的一句
requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping,
複製程式碼
進入檢視defaultRequestMapping方法
public final class func defaultRequestMapping(for endpoint: Endpoint<Target>, 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)))
}
}
複製程式碼
和endpointClosure類似,我們經過轉換,可以得到requestClosure的表示式為
{(endpoint:Endpoint<Target>, closure:RequestResultClosure) in
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)))
}
}
複製程式碼
整體上使用do-catch語句來初始化一個urlRequest,根據不同結果向閉包傳入不同的引數。一開始使用try來呼叫endpoint.urlRequest(),如果丟擲錯誤,會切換到catch語句中去。endpoint.urlRequest()這個方法比較長,這裡就不放出來,感興趣可自行到Moya核心程式碼裡的Endpoint.swift裡檢視。它其實做的事情很簡單,就是根據前面說到的endpoint的那些屬性來初始化一個NSURLRequest的物件。
以上就是上方圖中所畫的,根據TargetType最終生成Request的過程。很多人會感到疑惑,為什麼搞得這麼麻煩,直接一步到位,傳一些必要引數生成Request不就完了?為什麼還要再增加endPoint這麼一個節點?根據Endpoint類所提供的一些方法來看,個人認為應該是為了更靈活地配置網路請求,以適應更多樣化的業務需求。Endpoint類還有幾個方法
/// 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<Target>
/// Convenience method for creating a new `Endpoint` with the same properties as the receiver, but with replaced `task` parameter.
open func replacing(task: Task) -> Endpoint<Target>
複製程式碼
借用這些方法,在endpointClosure中可以給一些網路請求新增請求頭,替換請求引數,讓這些請求配置更加靈活。
我們看完了整個Request生成過程,那麼通過requestClosure生成的的Request是如何被外部拿到的呢?這就是我們下一步要探討的,Provider傳送請求實現過程。在下一節裡將會看到如何使用這個Request。
Provider傳送請求
我們再來看一下官方文件裡說明的Moya的基本使用步驟
- 建立列舉,遵守TargetType協議,實現規定的屬性。
- 初始化
provider = MoyaProvider<Myservice>()
- 呼叫provider.request,在閉包裡處理請求結果。
其中第一步我們在上方已經說明完了,MoyaProvider的初始化我們只說明瞭一小部分。在此不準備一口氣初始化方法中剩餘的部分講完,這又會涉及很多東西,同時理解起來會比較麻煩。在後面的程式碼解讀中,如果有涉及到相關屬性,再回到初始化方法中一個一個突破。
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)
}
複製程式碼
直接從這裡可能看不出什麼,再追溯到requestNormal
中去
這個方法內容比較長,其中一些外掛相關的程式碼,和測試樁的程式碼,暫且跳過不做說明,暫時不懂他們並不會成為理解provider.request的阻礙,它們屬於可選內容,而不是必須的。
let endpoint = self.endpoint(target)
複製程式碼
生成了endPoint物件,這個很好理解,前面已經做過說明。
檢視performNetworking
閉包
if cancellableToken.isCancelled {
self.cancelCompletion(pluginsWithCompletion, target: target)
return
}
複製程式碼
如果取消請求,則呼叫取消完成的回撥,並return,不在執行閉包內下面的語句。
在這個閉包裡傳入了引數(requestResult: Result<URLRequest, MoyaError>)
,這裡用到了Result,想深入瞭解,可自行研究,這裡簡單說一下Result是幹什麼的。Result使用列舉方式,提供一些執行處理的結果,如下,很容易能看懂它所表達的意思。
switch requestResult {
case .success(let urlRequest):
request = urlRequest
case .failure(let error):
pluginsWithCompletion(.failure(error))
return
}
複製程式碼
如果請求成功,會拿到URLRequest,如果失敗,會使用外掛去處理失敗回撥。
// Allow plugins to modify request
let preparedRequest = self.plugins.reduce(request) { $1.prepare($0, target: target) }
複製程式碼
使用外掛對請求進行完善
cancellableToken.innerCancellable = self.performRequest(target, request: preparedRequest, callbackQueue: callbackQueue, progress: progress, completion: networkCompletion, endpoint: endpoint, stubBehavior: stubBehavior)
複製程式碼
這裡的self.performRequest
就是進行實際的網路請求,內部程式碼比較多,但是思路很簡單,使用Alamofire的SessionManager來傳送請求。
配置完成後就可以呼叫requestClosure(endpoint, performNetworking)
,執行這個閉包獲取到上方所說的Request,來執行具體的網路請求了。
Response
在使用Alamofire傳送請求時,定義了閉包來處理請求的響應。Response這個類對於請求結果,提供了一些加工方法,比如data轉json,圖片轉換等。
Plugins
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>
複製程式碼
prepare
可以在請求之前對request進行修改。willSend
在請求傳送之前的一瞬間呼叫,這個可以用來新增請求時轉圈圈的ToastdidReceive
在接收到請求響應時,且MoyaProvider的completion handler之前呼叫。process
在completion handler之前呼叫,用來修改請求結果
可以通過以下圖來直觀地理解外掛呼叫時機使用外掛的方式,讓程式碼僅保持著主幹邏輯,使用者根據業務需求自行加入外掛來配置自己的網路業務層,這樣做更加靈活,低耦合。Moya提供了4種外掛
- AccessTokenPlugin OAuth的Token驗證
- CredentialsPlugin 證書
- NetworkActivityPlugin 網路請求狀態
- NetworkLoggerPlugin 網路日誌
可以根據需求編寫自己的外掛,選取NetworkActivityPlugin來檢視外掛內部構成。
public final class NetworkActivityPlugin: PluginType {
public typealias NetworkActivityClosure = (_ change: NetworkActivityChangeType, _ target: TargetType) -> Void
let networkActivityClosure: NetworkActivityClosure
public init(networkActivityClosure: @escaping NetworkActivityClosure) {
self.networkActivityClosure = networkActivityClosure
}
public func willSend(_ request: RequestType, target: TargetType) {
networkActivityClosure(.began, target)
}
public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
networkActivityClosure(.ended, target)
}
}
複製程式碼
外掛內部結構很簡單,除了自行定義的一些變數外,就是遵守PluginType
協議後,去實現協議規定的方法,在特定方法內做自己需要做的事。因為PluginType
它已經有一個協議擴充套件,把方法的預設實現都完成了,在具體外掛內不一定需要實現所有的協議方法,僅根據需要實現特定方法即可。
寫好外掛之後,使用起來也比較簡答,MoyaProvider的初始化方法中,有個形參plugins: [PluginType] = []
,把網路請求中需要用到的外掛加入陣列中。
總結
Moya可以說是非常Swift式的一個框架,最大的優點是使用面向協議的思想,讓使用者能以搭積木的方式配置自己的網路抽象層。提供了外掛機制,在保持主幹網路請求邏輯的前提下,讓開發者根據自身業務需求,定製自己的外掛,在合適的位置加入到網路請求的過程中。