上一篇講了 Moya
構建和發起請求的資料流,從 Target -> Endpoint -> Request
這一套路清晰明瞭。現在我們來講講 Moya
資料返回的流程。再一次祭出那張圖(圖片來自參考2)。
好了,看完這張圖就可以關了,下面基本上可以不用看了。如有閒情,那就再聽我嘮叨一下。
在這裡,我們依然跟上一篇一樣,避開各種錯誤分支流程。
接收資料及回傳
在 Moya
層發出資料請求後,剩下的工作就是 Alamofire
去處理了。至於 Alamofire
如何發起請求以及接收響應,有興趣可以去研究下程式碼或者看這方面的程式碼分析,在這不多講了。我們只考慮 Moya
這一層的處理。
我們在發起請求的位置可以找到接收響應資料的程式碼:
progressAlamoRequest = progressAlamoRequest.response(callbackQueue: callbackQueue, completionHandler: completionHandler)
progressAlamoRequest.resume()
複製程式碼
應該比較熟悉了,在這裡可以看到接收響應的處理器 completionHandler
。在這個處理器裡,首先會把一個響應相關的資訊丟到 convertResponseToResult()
方法裡面做個包裝,把這些資訊封裝在一個 Response
物件裡面,再打包到 Result
中。我們來看看 Response
物件:
public final class Response: CustomDebugStringConvertible, Equatable {
public let statusCode: Int // 狀態碼
public let data: Data // 資料
public let request: URLRequest? // 對應的請求物件
public let response: HTTPURLResponse? // 響應物件
......
}
複製程式碼
Response
類本體沒有太多資訊,不過它的擴充套件提供了不少有用的方法,包括根據響應狀態碼過濾請求,以及我們很關心的資料轉換。資料轉換一會我們單獨講,先把主流程梳理一下。
從 convertResponseToResult()
返回的 Result
會被傳入 completion()
回撥中,
let completionHandler: RequestableCompletion = { response, request, data, error in
let result = convertResponseToResult(response, request: request, data: data, error: error)
// Inform all plugins about the response
......
completion(result)
}
複製程式碼
這個 completion
是從 requestNormal()
中傳進來的,我們來看看。
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)
}
}
複製程式碼
我們只關注 pluginsWithCompletion()
:
let pluginsWithCompletion: Moya.Completion = { result in
let processedResult = self.plugins.reduce(result) { $1.process($0, target: target) }
completion(processedResult)
}
複製程式碼
這裡通過外掛對 result
進行處理後,最後呼叫 completion()
,這個 completion
就是由業務層程式碼傳進來的回撥了。嗯,終於回了口氣。來看看呼叫:
gitHubProvider.request(.userRepositories(username)) { result in
......
}
複製程式碼
這樣就回到我們的業務程式碼了。至此整個資料流又回到了業務層。
轉換資料
業務層接收到資料後,就可以直接使用資料。不過這裡我們獲取到的是一個 Response
物件,也就是說我們獲取到的基本上是一個沒有經過多少處理的裸資料。對於業務層開發來講,將這些資料轉換為直接可以使用的物件或結構體是一個強需求。這也正是各種資料對映庫的用武之地。
有些網路層的封裝,可能會將這個對映操作直接耦合在網路封裝層,這樣返回給業務層的就是一個可以直接使用的資料物件或者資料物件陣列。不過 Moya
沒有這麼做,它甚至沒有把 Moya
轉換為 JSON
,回傳的只是裸資料,這樣做有以下好處:
- Moya 只需要關注網路請求,能保持輕量;
- 不與具體的資料轉換庫耦合,方便擴充套件,讓使用者決定怎麼去轉換資料;同時減少依賴庫;
- 回傳裸資料,讓使用者去定義介面的資料格式,方便擴充套件;
不過 Moya
也為我們提供了幾個轉換方法,如下:
mapImage()
嘗試把響應資料轉化為UIImage
例項 如果不成功將產生一個錯誤。mapJSON()
嘗試把響應資料對映成一個JSON
物件,如果不成功將產生一個錯誤。mapString()
把響應資料轉化成一個字串,如果不成功將產生一個錯誤。mapString(atKeyPath:)
嘗試把響應資料的key Path
對映成一個字串,如果不成功將產生一個錯誤。
在業務層可能能用得上這些方法,比如先將資料轉換成 JSON
,再丟給其它庫使用。
在 Github
上,Moya
提供了一些 JSON
序列化的庫,可以參考一下。不過 Swift 4
之後的 Codable
也許能統一江湖。
測試插樁
Moya
還有一個很好的特性,就是為本地 mock
資料提供了一個很好的支援。
要想使用本地 mock
功能,可以在建立 MoyaProvider
時傳入 stubClosure
引數,值為 MoyaProvider.immediatelyStub
或者 MoyaProvider.delayedStub
,其值被賦值給 MoyaProvider
的 stubClosure
屬性:
public typealias StubClosure = (Target) -> Moya.StubBehavior
/// A closure responsible for determining the stubbing behavior
/// of a request for a given `TargetType`.
open let stubClosure: StubClosure
複製程式碼
實際上是為了最終獲取 StubBehavior
。這個值對於 Target
到 Request
的構建過程沒有影響,只是影響到發起請求的操作。我們在此不再詳細描述,通過流程圖來看看:
- 在
performRequest()
中根據stubBehavior
來判斷,而進入stubRequest()
方法; - 在
stubRequest()
方法中,使用createStubFunction
建立stub()
閉包,並根據stubBehavior
來決定stub()
的執行方式和時機; - 最主要的操作是在
stub()
中,根據endpoint.sampleResponseClosure()
來處理sample
資料;
後面的流程就是資料返回了。
總結
至此,基於資料流,我們大概瀏覽了一下 Moya
的實現。當然還有一些功能沒有涉及到,如進度處理、請求跟蹤等,有興趣可以看下原始碼。
個人覺得 Moya
最有意思的就是通過 Target
和 enum
來描述一組介面,同時可以通過 MultiTarget
來將介面分組,給了我們很大的空間。不過有利有弊,enum
帶來便利的同時,也可能會帶來大量的 switch...case
程式碼,我們需要根據實際情況來組織程式碼。
參考
- 官方文件
https://github.com/Moya/Moya/blob/master/docs/README.md
- Moya的設計之道
https://github.com/LeoMobileDeveloper/Blogs/blob/master/Swift/AnaylizeMoya.md