专栏名称: 知识小集
目录
相关文章推荐
算法爱好者  ·  北京大学出的第二份 DeepSeek ... ·  9 小时前  
算法爱好者  ·  清北 DeepSeek 教程"神仙打架",北 ... ·  昨天  
九章算法  ·  一年被裁两次,一个底层码农的大落大起 ·  4 天前  
九章算法  ·  Meta学神刷题奥义!《LeetCode通关 ... ·  5 天前  
51好读  ›  专栏  ›  知识小集

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

知识小集  · 掘金  ·  · 2018-04-03 02:12

正文

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

Moya 是一个网络抽象层,默认是基于 Alamofire 的。网上已经有一些不错的原理分析及源码分析的文章,大家可以参考。在这我们从数据流的角度来粗略的描述一下 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 呢?

我有两个观点:

  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






请到「今天看啥」查看全文