从数据流角度管窥 Moya 的实现(一):构建请求

相信你们都封装过网络层。git

虽然系统提供的网络库以及一些著名的第三方网络库(AFNetworkingAlamofire)已经能知足各类 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。我把这个 MoyaProvider称为 Moya的操控者。在 Moya层,它是整个数据流的管理者,包括构建请求、发起请求、接收响应、处理响应。也许相似的,咱们本身封装的网络库也会有这样一个角色,如 NetworkManager。咱们来看看它和 Moya中其它类/结构体的关系。并发

 

 

与咱们直接打交道最多的也是这个类,不过咱们不在这细讲,在这里它不是主角。咱们来结合数据流,来看看数据在这个类中怎么流转。app

构建 Request

一个基本的 HTTP/HTTPS普通数据请求一般包含如下几个要素:框架

  • URL
  • 请求参数
  • 请求方法
  • 请求报头信息
  • 可选的认证信息

对于 Alamofire来讲,最终是构建一个 Request,而后使用不一样的请求对象,依赖于这些信息来发起请求。因此,构建请求的终点是 Requestless

不过官方文档给了一个构建 Request的流程图:

 

 

咱们来看看这个流程。

请求的起点 Target

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时,能够根据不一样的接口来配置不一样的 baseURLpathmethod等信息。不过可能会致使一个问题:在一个大的独立工程里面,一般接口有几十上百个。若是你把全部的接口都放一个枚举里面,你可能最后会发现,各类 switch会把这个文件撑得很长。因此,还须要根据实际状况来看看如何去划分咱们的接口,让代码分散在不一样的文件里面(MultiTarget专门用来干这事,能够研究一下)。

到这一步,咱们获得的数据是一个 Target枚举,它包含了构建一组请求所须要的信息。实际上,咱们主要的任务就是去定义这些枚举,后面的构建过程,若是没有特殊需求,基本上就是个黑盒了。

有了 Target,咱们就能够用具体的枚举值来发起请求了,

gitHubProvider.request(.userRepositories(username)) { result in ...... } 复制代码

大多数时候,业务层代码须要作的就是这些了。是否是很简单?

下面咱们来看看 Moya的黑盒子里面作了些什么?

Endpoint

按理说,咱们构建好 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呢?

我有两个观点:

  1. 比起 Target来,Endpoint更像一个请求对象;Target是经过枚举来描述的一组请求,而 Endpoint就是一个实实在在的请求对象;(废话)
  2. 经过 Endpoint来隔离业务代码与 Request,毕竟这是 Moya的目标

若是有不一样观点,还请告诉我。

重复上面一句话:咱们能够自定义转换方法,来执行 Target到 Endpoint的映射操做。不过还有个问题,有些代码(好比headers的设置)便可以放在 Target里面,也能够放在 Endpoint里面。我的观点是能放在 Target里面的就放在 Target里,这样不须要自已去定义 EndpointClosure

Endpoint类还有一些方法来便捷建立 Endpoint,能够参考一下。

到这一步,咱们获得的数据是一个 Endpoint对象,有了这个对象,咱们就能够来建立 Request了。

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闭包中。在这个闭包里,就开始了发起请求的旅程。咱们简单看一下流程:

 

 

基本上就三个步骤:

  1. performRequest():在这个方法中,将请求根据 task的类型分流;
  2. sendRequest()uploadFile()等四方法:这几个方法主要是建立对应的请求对象,如 DataRequestUploadRequest
  3. 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的实现,敬请关注。

参考

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

追踪一下 Moya 的数据流向,来看看它的基本实现。

做者:知识小集 连接:https://juejin.im/post/5ac2cf34f265da23a1421483 来源:掘金 著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。