從數據流角度管窺 Moya 的實現(一):構建請求

相信你們都封裝過網絡層。git

雖然系統提供的網絡庫以及一些著名的第三方網絡庫(AFNetworkingAlamofire)已經能知足各類 HTTP/HTTPS的網絡請求,但直接在代碼裏用起來,終歸是比較晦澀,不是那麼的順手。因此咱們都會傾向於根據本身的實際需求,再封裝一個更好用的網絡層,加入一些特殊處理。同時也讓業務代碼更好地與底層的網絡框架隔離和解耦。github

Moya實際上作的就是這樣一件事,它在 Alamofire的基礎上又封裝了一層,讓咱們沒必要處理過多的底層細節。按照官方文檔的說法:swift

It's less of a framework of code and more of a framework of how to think about network requests.api

對於應用層開發者來講,一個 HTTP/HTTPS的網絡請求流程很簡單,即客戶端發起請求,服務端接收到請求處理後再將響應數據回傳給客戶端。對於客戶端來講,大致只須要作兩件事:構建請求併發送、接收響應並處理。以下一個簡單的流程:網絡

 

 

咱們這裏從普通數據請求的整個流程來看看 Moya的基本實現。閉包

操控者 MoyaProvider

在梳理流程以前,有必要了解一下 MoyaProvider。我把這個 MoyaProvider稱爲 Moya的操控者。在 Moya層,它是整個數據流的管理者,包括構建請求、發起請求、接收響應、處理響應。也許相似的,咱們本身封裝的網絡庫也會有這樣一個角色,如 NetworkManager。咱們來看看它和 Moya中其它類/結構體的關係。併發

 

 

與咱們直接打交道最多的也是這個類,不過咱們不在這細講,在這裏它不是主角。咱們來結合數據流,來看看數據在這個類中怎麼流轉。app

構建 Request

一個基本的 HTTP/HTTPS普通數據請求一般包含如下幾個要素:框架

  • URL
  • 請求參數
  • 請求方法
  • 請求報頭信息
  • 可選的認證信息

對於 Alamofire來講,最終是構建一個 Request,而後使用不一樣的請求對象,依賴於這些信息來發起請求。因此,構建請求的終點是 Requestless

不過官方文檔給了一個構建 Request的流程圖:

 

 

咱們來看看這個流程。

請求的起點 Target

Target是構建一個請求的起點,它包含一個請求所須要的基本信息。不過一個 Target不是定義單一一個請求,而是定義一組相關的請求。這裏先了解一下 TargetType協議:

public protocol TargetType { var baseURL: URL { get } var path: String { get } var method: Moya.Method { get } /// Provides stub data for use in testing. var sampleData: Data { get } var task: Task { get } var validationType: ValidationType { get } var headers: [String: String]? { get } } 複製代碼

爲了控制篇幅,我把不須要的註釋都刪了,下同。sampleData主要是用於本地 mock數據,在文章中不作描述。

能夠看到這個協議包含了一個請求所須要的基本信息:用於拼接 URL的 baseURL和 path、請求方法、請求報頭等。咱們自定義的 Target必須實現這個接口,並根據須要設置請求信息,這個應該很好理解。

若是隻是描述一個請求的話,可能使用 struct會好一些;而若是是一組的話,那仍是用枚舉方便些(話說枚舉用得好很差,直接體現了 Swift水平好很差)。來看看官方的例子:

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")! } ...... } 複製代碼

這基本是標配。枚舉的關聯對象是請求所須要的參數,若是請求參數過多,最好放在一個字典裏面。

至於 task屬性,其類型 Task是一個枚舉,定義了請求的實際任務類型,好比說是普通的數據請求,仍是上傳下載。這個屬性能夠關注一下,由於請求的參數都是附在這個屬性上。

在擴展 TargetType時,能夠根據不一樣的接口來配置不一樣的 baseURLpathmethod等信息。不過可能會致使一個問題:在一個大的獨立工程裏面,一般接口有幾十上百個。若是你把全部的接口都放一個枚舉裏面,你可能最後會發現,各類 switch會把這個文件撐得很長。因此,還須要根據實際狀況來看看如何去劃分咱們的接口,讓代碼分散在不一樣的文件裏面(MultiTarget專門用來幹這事,能夠研究一下)。

到這一步,咱們獲得的數據是一個 Target枚舉,它包含了構建一組請求所須要的信息。實際上,咱們主要的任務就是去定義這些枚舉,後面的構建過程,若是沒有特殊需求,基本上就是個黑盒了。

有了 Target,咱們就能夠用具體的枚舉值來發起請求了,

gitHubProvider.request(.userRepositories(username)) { result in ...... } 複製代碼

大多數時候,業務層代碼須要作的就是這些了。是否是很簡單?

下面咱們來看看 Moya的黑盒子裏面作了些什麼?

Endpoint

按理說,咱們構建好 Target並把對應的信息丟給 MoyaProvider後,MoyaProvider直接去構建一個 Request,而後發起請求就好了。而在從上面的圖能夠看到,Target和 Request之間還有一個 Endpoint。這是啥玩意呢?咱們來看看。

在 MoyaProvider的 request方法中調用了 requestNormal方法。這個方法的第一行就作了個轉換操做,將 Target轉換成 Endpoint對象:

func requestNormal(_ target: Target, callbackQueue: DispatchQueue?, progress: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> Cancellable { let endpoint = self.endpoint(target) ...... } 複製代碼

endpoint()方法實際上調用的是 MoyaProvider的 endpointClosure屬性:

public typealias EndpointClosure = (Target) -> Endpoint open let endpointClosure: EndpointClosure open func endpoint(_ token: Target) -> Endpoint { return endpointClosure(token) } 複製代碼

EndpointClosure的用途實際上就是將 Target映射爲 Endpoint。咱們能夠自定義轉換方法,並在初始 MoyaProvider時傳遞給 endpointClosure參數,像這樣:

let endpointClosure = { (target: MyTarget) -> Endpoint in let defaultEndpoint = MoyaProvider.defaultEndpointMapping(for: target) return defaultEndpoint.adding(newHTTPHeaderFields: ["APP_NAME": "MY_AWESOME_APP"]) } let provider = MoyaProvider<GitHub>(endpointClosure: endpointClosure) 複製代碼

若是不想自定義,那麼就用 Moya提供的默認轉換方法就行。

哦,還沒看 Endpoint到底長啥樣:

open class Endpoint { public typealias SampleResponseClosure = () -> EndpointSampleResponse open let url: String open let method: Moya.Method open let task: Task open let httpHeaderFields: [String: String]? ...... } 複製代碼

是否是以爲和 TargetType差很少?那問題來了,爲何要 Endpoint呢?

我有兩個觀點:

  1. 比起 Target來,Endpoint更像一個請求對象;Target是經過枚舉來描述的一組請求,而 Endpoint就是一個實實在在的請求對象;(廢話)
  2. 經過 Endpoint來隔離業務代碼與 Request,畢竟這是 Moya的目標

若是有不一樣觀點,還請告訴我。

重複上面一句話:咱們能夠自定義轉換方法,來執行 Target到 Endpoint的映射操做。不過還有個問題,有些代碼(好比headers的設置)便可以放在 Target裏面,也能夠放在 Endpoint裏面。我的觀點是能放在 Target裏面的就放在 Target裏,這樣不須要自已去定義 EndpointClosure

Endpoint類還有一些方法來便捷建立 Endpoint,能夠參考一下。

到這一步,咱們獲得的數據是一個 Endpoint對象,有了這個對象,咱們就能夠來建立 Request了。

Request

和 Target->Endpoint的映射同樣,Endpoint->Request的映射也有一個相似的屬性:requestClosure屬性。

public typealias RequestClosure = (Endpoint, @escaping RequestResultClosure) -> Void open let requestClosure: RequestClosure 複製代碼

一樣也能夠自定義閉包傳遞給 MoyaProvider的構造器,但一般不建議這麼作。由於這樣會讓業務代碼直接觸達 Request,有違 Moya的目標。一般咱們直接用默認的轉換方法就行。默認映射方法的實如今 MoyaProvider+Defaults.swift文件中,以下:

public final class func defaultRequestMapping(for endpoint: Endpoint, closure: RequestResultClosure) { do { let urlRequest = try endpoint.urlRequest() closure(.success(urlRequest)) } ...... } 複製代碼

看代碼會發現實際的轉換是由 Endpoint類的 urlRequest方法來完成的,以下:

public func urlRequest() throws -> URLRequest { guard let requestURL = Foundation.URL(string: url) else { throw MoyaError.requestMapping(url) } var request = URLRequest(url: requestURL) request.httpMethod = method.rawValue request.allHTTPHeaderFields = httpHeaderFields switch task { case .requestPlain, .uploadFile, .uploadMultipart, .downloadDestination: return request case .requestData(let data): request.httpBody = data return request ...... } 複製代碼

這個方法建立了一個 URLRequest對象,看代碼都能理解。

返回到 defaultRequestMapping()方法中,能夠看到生成的 urlRequest被附在一個 Result枚舉中,並傳給 defaultRequestMapping的第二個參數: RequestResultClosure。這步咱們暫時到這。

到此咱們的 URLRequest對象就構建完成了,實際上咱們會發現 URLRequest包含的信息並不大,但已經足夠了,能夠發起請求了。

發起請求

咱們回到 RequestResultClosure中,也就是 requestNormal()方法的 performNetworking閉包中。在這個閉包裏,就開始了發起請求的旅程。咱們簡單看一下流程:

 

 

基本上就三個步驟:

  1. performRequest():在這個方法中,將請求根據 task的類型分流;
  2. sendRequest()uploadFile()等四方法:這幾個方法主要是建立對應的請求對象,如 DataRequestUploadRequest
  3. sendAlamofireRequest():各種請求最後會匯聚到這個方法中,完成發起請求操做;
func sendAlamofireRequest<T>(_ alamoRequest: T, target: Target, callbackQueue: DispatchQueue?, progress progressCompletion: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> CancellableToken where T: Requestable, T: Request { ...... progressAlamoRequest = progressAlamoRequest.response(callbackQueue: callbackQueue, completionHandler: completionHandler) progressAlamoRequest.resume() ...... } 複製代碼

到此爲止,請求部分就基本結束了。

有一個小問題能夠注意下:一個 Target最後一直被傳遞到 sendAlamofireRequest方法中,比 Endpoint的使用週期還長。呵呵。

等等,還有件事

爲何 Target的使用週期比 Endpoint還長呢?看代碼,在 sendAlamofireRequest()方法中有這麼一段:

let plugins = self.plugins plugins.forEach { $0.willSend(alamoRequest, target: target) } 複製代碼

也就是說 Target須要用在 plugin的方法中。Plugin,即插件,是 Moya提供了一種特別實用的機制,能夠被用來編輯請求、響應及完成反作用的。Moya提供了幾個默認的插件,一樣咱們也能夠自定義插件。全部的插件都須要實現 PluginType協議,看看它的定義:

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 } } 複製代碼

實際上就是在整個數據流四個位置插入一些操做,這些操做能夠對數據進行修改,也能夠是一些沒有反作用(例如日誌)的操做。實際上 prepare操做是在 RequestResultClosure中就執行了。後面兩個方法都是在響應階段插入的操做。在此不描述了。

總結

這篇文章主要是從數據的流向來看了看 Moya的請求構建過程。咱們避開了各類產生錯誤的分支以及用於測試插樁的代碼,這些有興趣能夠參考代碼的具體實現。

最後盜圖一張,你就會發現一圖勝千言,我上面講的以及後面一篇文章講的全是廢話。

 

 

下一篇咱們會從數據流的後半段 -- 響應處理-- 來繼續看看 Moya的實現,敬請關注。

參考

  1. 官方文檔 https://github.com/Moya/Moya/blob/master/docs/README.md
  2. Moya的設計之道 https://github.com/LeoMobileDeveloper/Blogs/blob/master/Swift/AnaylizeMoya.md

追蹤一下 Moya 的數據流向,來看看它的基本實現。

做者:知識小集 連接:https://juejin.im/post/5ac2cf34f265da23a1421483 來源:掘金 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
相關文章
相關標籤/搜索