從資料流角度管窺 Moya 的實現(二):處理響應

知識小集發表於2019-03-04

上一篇講了 Moya 構建和發起請求的資料流,從 Target -> Endpoint -> Request 這一套路清晰明瞭。現在我們來講講 Moya 資料返回的流程。再一次祭出那張圖(圖片來自參考2)。

從資料流角度管窺 Moya 的實現(二):處理響應

好了,看完這張圖就可以關了,下面基本上可以不用看了。如有閒情,那就再聽我嘮叨一下。

在這裡,我們依然跟上一篇一樣,避開各種錯誤分支流程。

接收資料及回傳

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 ,回傳的只是裸資料,這樣做有以下好處:

  1. Moya 只需要關注網路請求,能保持輕量;
  2. 不與具體的資料轉換庫耦合,方便擴充套件,讓使用者決定怎麼去轉換資料;同時減少依賴庫;
  3. 回傳裸資料,讓使用者去定義介面的資料格式,方便擴充套件;

不過 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,其值被賦值給 MoyaProviderstubClosure 屬性:

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。這個值對於 TargetRequest 的構建過程沒有影響,只是影響到發起請求的操作。我們在此不再詳細描述,通過流程圖來看看:

從資料流角度管窺 Moya 的實現(二):處理響應

  • performRequest() 中根據 stubBehavior 來判斷,而進入 stubRequest() 方法;
  • stubRequest() 方法中,使用 createStubFunction 建立 stub() 閉包,並根據 stubBehavior 來決定 stub() 的執行方式和時機;
  • 最主要的操作是在 stub() 中,根據 endpoint.sampleResponseClosure() 來處理 sample 資料;

後面的流程就是資料返回了。

總結

至此,基於資料流,我們大概瀏覽了一下 Moya 的實現。當然還有一些功能沒有涉及到,如進度處理、請求跟蹤等,有興趣可以看下原始碼。

個人覺得 Moya 最有意思的就是通過 Targetenum 來描述一組介面,同時可以通過 MultiTarget 來將介面分組,給了我們很大的空間。不過有利有弊,enum 帶來便利的同時,也可能會帶來大量的 switch...case 程式碼,我們需要根據實際情況來組織程式碼。

參考

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

掃描關注 知識小集

從資料流角度管窺 Moya 的實現(二):處理響應

相關文章