相信你們都封裝過網絡層。git
雖然系統提供的網絡庫以及一些著名的第三方網絡庫(AFNetworking
, Alamofire
)已經能知足各類 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
稱爲 Moya
的操控者。在 Moya
層,它是整個數據流的管理者,包括構建請求、發起請求、接收響應、處理響應。也許相似的,咱們本身封裝的網絡庫也會有這樣一個角色,如 NetworkManager
。咱們來看看它和 Moya
中其它類/結構體的關係。併發
與咱們直接打交道最多的也是這個類,不過咱們不在這細講,在這裏它不是主角。咱們來結合數據流,來看看數據在這個類中怎麼流轉。app
一個基本的 HTTP/HTTPS
普通數據請求一般包含如下幾個要素:框架
對於 Alamofire
來講,最終是構建一個 Request
,而後使用不一樣的請求對象,依賴於這些信息來發起請求。因此,構建請求的終點是 Request
。less
不過官方文檔給了一個構建 Request
的流程圖:
咱們來看看這個流程。
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
時,能夠根據不一樣的接口來配置不一樣的 baseURL
、path
、method
等信息。不過可能會致使一個問題:在一個大的獨立工程裏面,一般接口有幾十上百個。若是你把全部的接口都放一個枚舉裏面,你可能最後會發現,各類 switch
會把這個文件撐得很長。因此,還須要根據實際狀況來看看如何去劃分咱們的接口,讓代碼分散在不一樣的文件裏面(MultiTarget
專門用來幹這事,能夠研究一下)。
到這一步,咱們獲得的數據是一個
Target
枚舉,它包含了構建一組請求所須要的信息。實際上,咱們主要的任務就是去定義這些枚舉,後面的構建過程,若是沒有特殊需求,基本上就是個黑盒了。
有了 Target
,咱們就能夠用具體的枚舉值來發起請求了,
gitHubProvider.request(.userRepositories(username)) { result in ...... } 複製代碼
大多數時候,業務層代碼須要作的就是這些了。是否是很簡單?
下面咱們來看看 Moya
的黑盒子裏面作了些什麼?
按理說,咱們構建好 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
呢?
我有兩個觀點:
Target
來,Endpoint
更像一個請求對象;Target
是經過枚舉來描述的一組請求,而 Endpoint
就是一個實實在在的請求對象;(廢話)Endpoint
來隔離業務代碼與 Request
,畢竟這是 Moya
的目標若是有不一樣觀點,還請告訴我。
重複上面一句話:咱們能夠自定義轉換方法,來執行 Target
到 Endpoint
的映射操做。不過還有個問題,有些代碼(好比headers的設置)便可以放在 Target
裏面,也能夠放在 Endpoint
裏面。我的觀點是能放在 Target
裏面的就放在 Target
裏,這樣不須要自已去定義 EndpointClosure
。
Endpoint
類還有一些方法來便捷建立 Endpoint
,能夠參考一下。
到這一步,咱們獲得的數據是一個
Endpoint
對象,有了這個對象,咱們就能夠來建立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
閉包中。在這個閉包裏,就開始了發起請求的旅程。咱們簡單看一下流程:
基本上就三個步驟:
performRequest()
:在這個方法中,將請求根據 task
的類型分流;sendRequest()
、uploadFile()
等四方法:這幾個方法主要是建立對應的請求對象,如 DataRequest
、UploadRequest
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
的實現,敬請關注。
https://github.com/Moya/Moya/blob/master/docs/README.md
https://github.com/LeoMobileDeveloper/Blogs/blob/master/Swift/AnaylizeMoya.md
追蹤一下 Moya 的數據流向,來看看它的基本實現。