当前位置: 首页 > 工具软件 > Moya > 使用案例 >

从数据流角度管窥 Moya 的实现(二):处理响应

黄朗
2023-12-01

上一篇讲了 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 ,回传的只是裸数据,这样做有以下好处:

  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 的构建过程没有影响,只是影响到发起请求的操作。我们在此不再详细描述,通过流程图来看看:

  • 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

扫描关注 知识小集

 类似资料: