上一篇讲了 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