阅读完 Moya 后, 对于 Moya 的感触就是函数式编程, 无处不在的面向协议编程以及插件化的思想.
Moya 的原理已经有很多文章都详细分析过了, 本文主要分析下 Moya 中那些值得学习借鉴的设计.
TargetType - 请求参数协议化
在官方示例中, 我们需要使用 Provider
来调用网络请求. 使用代码如下:
let provider = MoyaProvider<LoginAPI>()
provider.request(.login) { ... }
复制代码
非常简单明了的调用, 它也是调用 Moya 的唯一入口.
其中 LoginAPI
是一个遵循于 TargetType
协议的枚举类型, 用来定义网络请求中的基本参数和行为。
TargetType
协议如下:
public protocol TargetType {
var baseURL: URL { get }
var path: String { get }
var method: Method { get }
var parameters: [String: AnyObject]? { get }
var sampleData: Data { get }
...
}
// Implement a target
enum LoginAPI {
enum authenticate
enum login
}
extension LoginAPI: TargetType {
var baseURL: NSURL{ return URL(string: "http://foo.cn") }
var path: String {
switch self {
case authenticate: return "/authenticate"
case login: return "/login"
}
}
...
}
复制代码
这种方式的好处显而易见, 完全利用面向协议的方式来定义网络请求的参数, 且使用枚举类型可以很方便地管理一组 API. 属性使用只读修饰, 更保证了值类型编程的状态可控性.
Provider
将按枚举类型从 LoginAPI
中获取参数, 并组合成一个 Request
, 最后通过 Alamofire 发送.
Provider 构造函数 - 易用性与灵活性共存的函数式编程
先来看一下 Provider
的构造函数:
public init(endpointClosure: @escaping EndpointClosure = Provider.defaultEndpointMapping,
requestClosure: @escaping RequestClosure = Provider.defaultRequestMapping,
stubClosure: @escaping StubClosure = Provider.neverStub,
callbackQueue: DispatchQueue? = nil,
manager: Manager = Manager.default,
plugins: [PluginType] = [],
trackInflights: Bool = false)
复制代码
Provider
的构造函数包含了多个闭包. 简单看下各个参数的用处:
endpointClosure
: 提供默认实现, 返回 TargetType
初始化的 Endpoint
对象, 也可以返回自定义的 Endpoint
.
requestClosure
: 由 Endpoint
获取 Request
进行网络请求的发送, 也可以自定义 Request
发送.
stubClosure
: 决定启用何种 stub 策略, 可以在单元测试时开启.
plugins
: 插件, 在下文中会详细说到它.
综上, 不难看出 Provider
真正做的事情就是:
既然 TargetType
已经具备了一个网络请求的所有参数, 那么为什么定义了 TargetType
后又要定义 EndpointClosure
和 RequestClosure
呢? 这种看似累赘的设计, 其实是为了实现插件的功能.
首先官方建议将 TargetType
使用枚举类型来实现, 同时所有请求参数定义为只读, 这样对于一个请求, 它的初始参数将是唯一确定的, 严格控制了其状态.
同时, Moya 允许用户在 TargetType
生成 Endpoint
的过程和 Endpoint
生成 Request
的过程中通过闭包参数传递处理逻辑, 其中 EndpointClosure
允许用户在原来的 TargetType
基础上进行参数的修改, 比如在这个阶段临时添加一个 HTTPHeader 或者添加分页信息. RequestClosure
允许用户在 Endpoint
生成 URLRequest
的过程中拥有最后一次处理机会, 你可以在这个阶段修改 cookie
,自定义头部字段等.
其实, 你在初始化 Provider
时不使用任何参数就可以满足日常需求, 它所提供的默认实现已经帮你完成了全部的转换. 而在用户有需求时, 可以在构造函数的闭包参数中插入修正代码, 这使得它非常灵活.
不需要额外处理的情况下, 用户可以忽略构造函数中的所有参数直接创建 Provider, 这大大提高了构造函数的易用性. 同时用户也可以在其中插入自定义的处理逻辑, 为构造函数提拱了足够的灵活性.
Stub - 易测试的网络层
单元测试中的一个重要概念就是 Stub(桩): 很多时候你需要的数据并不能简单地获取到, 它可能需要一个网络请求, 可能需要一次数据库查询, 也可能需要繁杂的构造条件, 所以我们需要使用 Stub 来提供默认的数据, 在需要时直接或延迟返回假数据即可.
Provider
中的 stubClosure
就是为单元测试提供的开关, 用户可以在 stubClosure
中返回需要的 stub 类型:
public enum StubBehavior {
case never
case immediate
case delayed(seconds: TimeInterval)
}
复制代码
它所返回的假数据由 TargetType
和 Endpoint
中的 sampleData
提供.
在构造函数中提供单元测试开关, 同时允许在构造函数内添加假数据, 使得 Moya 的网络请求测试变得异常简单.
Plugin - 插件化思想
从 TargetType->Endpoint->Request
的过程我们就看出了 Moya 插件化的思想, Plugin 更是将插件化实施落地, Provider
提供的插件参数是一个由遵循于 PluginType
的实例组成的集合, 它主要用于在网络请求的关键点进行事务处理. 我们先看下 PluginType
协议的内容:
public protocol PluginType {
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest
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>
}
复制代码
它定义了四个调用插件的节点, 下图是调用的流程图:
在四个 Plugin 节点, Moya 都会遍历 plugins
参数内的所有插件, 去执行各自的协议方法.
Moya 为常用的日志记录、加载时 loading 等功能添加了 NetworkLoggerPlugin 和 NetworkActivityPlugin 等四个可以直接使用的插件.
插件的做法像是将我们平时使用的代理回调抽象成了协议, 但这样做更利于扩展.
我们可以很方便地实现 PluginType
来自定义插件, 并实现对应的节点方法, 其他未实现的节点方法将会调用默认的扩展方法, 他们不会造成任何副作用. 这样的做法让网络层更加纯粹, 只注重于高层面的网络抽象, 将其他的处理逻辑用插件的方式开放给用户自定义.
Cancellable - 单一职责的请求取消协议
网络请求在很多场景下是需要取消的. 如: 在退出当前页面后, 当前页面正在进行的用于展示的数据请求应该被取消掉.
Moya 为每一个 request 函数提供了一个返回值, 返回值的类型是 Cancellable
:
public protocol Cancellable {
var isCancelled: Bool { get }
func cancel()
}
复制代码
它只有一个只读属性和一个 cancel
方法, 自定义遵循 Cancellable
协议时, 建议将 isCancelled
属性声明为:
public private(set) var isCancelled: Bool = false
复制代码
可以确保 isCancelled
属性只在 cancel
方法内部被改变, 对外为只读.
调用 Cancellable
的 cancel
方法最终是去调用 Alamofire.Request.cancel()
, 但是返回 Cancellable 可以让类型的目的非常明确, 该返回值只用于网络请求的取消, 满足单一职责原则.
Result - 灵活的错误处理思想
Moya 依赖了两个开源库, 除了 Alamofire, 它还依赖了 Result.
Result 非常简单, 总共只有 400 行左右代码, 它提供了一个错误处理的思想: 正确时返回数据, 错误时返回错误类型, 官方示例:
// Definition
typealias JSONObject = [String: Any]
enum JSONError: Error {
case noSuchKey(String)
case typeMismatch
}
func stringForKey(json: JSONObject, key: String) -> Result<String, JSONError> {
guard let value = json[key] else {
return .failure(.noSuchKey(key))
}
if let value = value as? String {
return .success(value)
}
else {
return .failure(.typeMismatch)
}
}
// Usage
switch stringForKey(json, key: "email") {
case let .success(email):
print("The email is \(email)")
case let .failure(.noSuchKey(key)):
print("\(key) is not a valid key")
case .failure(.typeMismatch):
print("Didn't have the right type")
}
复制代码
AFNetworking 提供了错误回调和成功回调分别对请求结果进行处理, 但实际使用中我们经常会在错误和成功后都执行某些代码或共享某些状态, 如果使用两个回调, 则必须写两份代码.
Result 和 AFNetworking 不一样, Result 则一次性返回正确或错误结果, 用户可以使用 Switch 语句在一个作用域内处理结果, 更加灵活.
总结
Alamofire 已经近乎于是官方使用的网络框架, 在其上最热门的网络抽象库就是 Moya. 我想这完全得益于它满足了人们对于网络抽象层的需求: 可测试型, 易用性, 可扩展性.