从数据流角度管窥 Moya 的实现(一):构建请求
相信大家都封装过网络层。
虽然系统提供的网络库以及一些著名的第三方网络库(AFNetworking, Alamofire)已经能满足各种 HTTP/HTTPS的网络请求,但直接在代码里用起来,终归是比较晦涩,不是那么的顺手。所以我们都会倾向于根据自己的实际需求,再封装一个更好用的网络层,加入一些特殊处理。同时也让业务代码更好地与底层的网络框架隔离和解耦。
Moya实际上做的就是这样一件事,它在 Alamofire的基础上又封装了一层,让我们不必处理过多的底层细节。按照官方文档的说法:
It's less of a framework of code and more of a framework of how to think about network requests.
对于应用层开发者来说,一个 HTTP/HTTPS的网络请求流程很简单,即客户端发起请求,服务端接收到请求处理后再将响应数据回传给客户端。对于客户端来说,大体只需要做两件事:构建请求并发送、接收响应并处理。如下一个简单的流程:
我们这里从普通数据请求的整个流程来看看 Moya的基本实现。
操控者 MoyaProvider
在梳理流程之前,有必要了解一下 MoyaProvider。我把这个 MoyaProvider称为 Moya的操控者。在 Moya层,它是整个数据流的管理者,包括构建请求、发起请求、接收响应、处理响应。也许类似的,我们自己封装的网络库也会有这样一个角色,如 NetworkManager。我们来看看它和 Moya中其它类/结构体的关系。
与我们直接打交道最多的也是这个类,不过我们不在这细讲,在这里它不是主角。我们来结合数据流,来看看数据在这个类中怎么流转。
构建 Request
一个基本的 HTTP/HTTPS普通数据请求通常包含以下几个要素:
- URL
- 请求参数
- 请求方法
- 请求报头信息
- 可选的认证信息
对于 Alamofire来说,最终是构建一个 Request,然后使用不同的请求对象,依赖于这些信息来发起请求。所以,构建请求的终点是 Request。
不过官方文档给了一个构建 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时,可以根据不同的接口来配置不同的 baseURL、path、method等信息。不过可能会导致一个问题:在一个大的独立工程里面,通常接口有几十上百个。如果你把所有的接口都放一个枚举里面,你可能最后会发现,各种 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呢?
我有两个观点:
- 比起
Target来,Endpoint更像一个请求对象;Target是通过枚举来描述的一组请求,而Endpoint就是一个实实在在的请求对象;(废话) - 通过
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闭包中。在这个闭包里,就开始了发起请求的旅程。我们简单看一下流程:
基本上就三个步骤:
performRequest():在这个方法中,将请求根据task的类型分流;sendRequest()、uploadFile()等四方法:这几个方法主要是创建对应的请求对象,如DataRequest、UploadRequestsendAlamofireRequest():各类请求最后会汇聚到这个方法中,完成发起请求操作;
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的实现,敬请关注。
参考
- 官方文档
https://github.com/Moya/Moya/blob/master/docs/README.md - Moya的设计之道
https://github.com/LeoMobileDeveloper/Blogs/blob/master/Swift/AnaylizeMoya.md
追踪一下 Moya 的数据流向,来看看它的基本实现。
作者:知识小集
链接:https://juejin.im/post/5ac2cf34f265da23a1421483
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
从数据流角度管窥 Moya 的实现(一):构建请求的更多相关文章
- Moya 浅析
Moya是一个高度抽象的网络库,他的理念是让你不用关心网络请求的底层的实现细节,只用定义你关心的业务.且Moya采用桥接和组合来进行封装(默认桥接了Alamofire),使得Moya非常好扩展,让你不 ...
- Akka(19): Stream:组合数据流,组合共用-Graph modular composition
akka-stream的Graph是一种运算方案,它可能代表某种简单的线性数据流图如:Source/Flow/Sink,也可能是由更基础的流图组合而成相对复杂点的某种复合流图,而这个复合流图本身又可以 ...
- 从消费者角度评估RestFul的意义
相关博文: 从消费者角度评估RestFul的意义 SpringBoot 构建RestFul API 含单元测试 REST是目前业界相当火热的术语,似乎发布的API不带个REST前缀,你都不好意思和别人 ...
- Swift网络封装库Moya中文手册之Endpoints
Endpoints Endpoint是一种半私有的数据结构,Moya用来解释网络请求的根本构成.一个endpoint储存了以下数据: The URL. The HTTP method (GET,POS ...
- 基于Moya、RxSwift和ObjectMapper优雅实现REST API请求
在Android开发中有非常强大的 Retrofit 请求,结合RxJava可以非常方便实现 RESTful API 网络请求.在 iOS开发中也有非常强大的网络请求库 Moya ,Moya是一个基于 ...
- 从nsurlsession、Alamofire到moya
更好的理解(抽象).更少的构建(配置).更方便的表达(语言) 一.iOS系统的网络编程(DSL概念) ios缺省的网络编程只是给出了网络编程的基本概念: urlsession.request.resp ...
- 从性能角度帮你理解HTTP协议
因为做性能测试分析的人来说,HTTP 协议可能是绕不过去的一个槛.在讲 HTTP 之前,我们得先知道一些基本的信息. HTTP(HyperText Transfer Protocol,超文本传输协议) ...
- Java的IO系统
Java IO系统 "对语言设计人员来说,创建好的输入/输出系统是一项特别困难的任务." 由于存在大量不同的设计方案,所以该任务的困难性是很容易证明的.其中最大的 ...
- ZeroMQ 教程 002 : 高级技巧
本文主要译自 zguide - chapter two. 但并不是照本翻译. 上一章我们简单的介绍了一个ZMQ, 并给出了三个套路的例子: 请求-回应, 订阅-发布, 流水线(分治). 这一章, 我们 ...
随机推荐
- Macbook使用Gitlab配置SSH Key
git是分布式代码管理工具,远程代码管理是基于ssh的,代码上传大搜gitlab或者github代码仓储时,需要进行ssh配置. 把本地代码上传到服务器时需要加密处理,git中公钥(id_rsa.pu ...
- knime 设置 小数点精度
kinme 默认小数精度是保留三位小数. 如果0.0003,knime会自动舍弃,读出0.下面步骤教你怎么把小数精度全部显示. File->references->preferred re ...
- console命令详解:(转载学习)
Console命令详解,让调试js代码变得更简单 Firebug是网页开发的利器,能够极大地提升工作效率. 但是,它不太容易上手.我曾经翻译过一篇<Firebug入门指南>,介绍了一些 ...
- 九度oj题目1027:欧拉回路
题目1027:欧拉回路 时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:2844 解决:1432 题目描述: 欧拉回路是指不令笔离开纸面,可画过图中每条边仅一次,且可以回到起点的一条 ...
- JavaScript字符串去除空格
/*字符串去除空格*/ String.prototype.Trim = function() { return this.replace(/(^\s*)|(\s*$)/g, "") ...
- artDialog组件应用学习(二)
一.没有操作选项的对话框 预览: html前台引入代码:(之后各种效果对话框引入代码致,调用方法也一样,就不一一写入) <script type="text/javascript&qu ...
- MVC Request.UrlReferrer为null
使用情景,登录后返回登录前访问的页面. 这个时候用到了UrlReferrer var returnUrl = HttpContext.Current.Request.UrlReferrer != nu ...
- EFCodeFirst 各种命令整理
1.Enable-Migrations (创建迁移目录:Migrations,如果有多个数据上下文可以用 -ContextTypeName 命令迁移对应的数据上下文 ) 2.Add-Migratio ...
- Java内部类详解 2
Java内部类详解 说起内部类这个词,想必很多人都不陌生,但是又会觉得不熟悉.原因是平时编写代码时可能用到的场景不多,用得最多的是在有事件监听的情况下,并且即使用到也很少去总结内部类的用法.今天我们就 ...
- 从零开始的全栈工程师——MySQL数据库( Dos命令 ) ( phpstudy )
MySQL是一个关系型数据库,存在表的概念.结构,数据库可以存放多张表,每个表里可以存放多个字段,每个字段可以存放多个记录. phpstudy使用终端打开数据库的命令行 密码: root 数据库 查看 ...