深刻理解Moya設計

關於Moya

Moya是一個網絡抽象層,它在底層將Alamofire進行封裝,對外提供更簡潔的接口供開發者調用。在以往的Objective-C中,大部分開發者會使用AFNetwork進行網絡請求,當業務複雜一些時,會對AFNetwork進行二次封裝,編寫一個適用於本身項目的網絡抽象層。在Objective-C中,有著名的YTKNetwork,它將AFNetworking封裝成抽象父類,而後根據每一種不一樣的網絡請求,都編寫不一樣的子類,子類繼承父類,來實現請求業務。Moya在項目層次中的地位,有點相似於YTKNetwork。能夠看下圖對比 git

可是若是單純把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 的邂逅 (下)github

Moya的模塊組成

因爲Moya是使用POP來設計的一個網絡抽象層,所以他總體的邏輯結構並無明顯的繼承關係。Moya的核心代碼,能夠分紅如下幾個模塊 編程

Provider

provider是一個提供網絡請求服務的提供者。經過一些初始化配置以後,在外部能夠直接用provider來發起request。json

Request

在使用Moya進行網絡請求時,第一步須要進行配置,來生成一個Request。首先按照官方文檔,建立一個枚舉,遵照TargetType協議,並實現協議所規定的屬性。爲何要建立枚舉來遵照協議,而不像Objective-C那樣建立類來遵照協議呢?其實使用類或者結構體也是能夠的,這裏猜想使用枚舉的緣由是由於swift的枚舉功能比Objective-C強大許多,枚舉結合switch語句,使得API管理起來比較方便。 Request的生成過程以下圖 swift

咱們根據上圖,結合代碼來分析其Request的生成過程。 根據建立了一個遵照TargetType協議的名爲Myservice的枚舉,咱們完成了以下幾個變量的設置。

baseURL
path
method
sampleData
task
headers
複製代碼

提供了這些網絡請求的「基本材料」以後,就能夠進一步配置去生成所須要的請求。看上圖的第一個箭頭,經過了一個EndpointClosure生成了endPoint。endPoit是一個對象,把網絡請求所需的一些屬性和方法進行了包裝,在EndPoint類中有以下屬性:api

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作了什麼。bash

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的基本使用步驟

  1. 建立枚舉,遵照TargetType協議,實現規定的屬性。
  2. 初始化 provider = MoyaProvider<Myservice>()
  3. 調用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在請求發送以前的一瞬間調用,這個能夠用來添加請求時轉圈圈的Toast
  • didReceive在接收到請求響應時,且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式的一個框架,最大的優勢是使用面向協議的思想,讓使用者能以搭積木的方式配置本身的網絡抽象層。提供了插件機制,在保持主幹網絡請求邏輯的前提下,讓開發者根據自身業務需求,定製本身的插件,在合適的位置加入到網絡請求的過程當中。

相關文章
相關標籤/搜索