Moya是一个网络抽象层,它在底层将Alamofire进行封装,对外提供更简洁的接口供开发者调用。在以往的Objective-C中,大部分开发者会使用AFNetwork进行网络请求,当业务复杂一些时,会对AFNetwork进行二次封装,编写一个适用于本身项目的网络抽象层。在Objective-C中,有著名的YTKNetwork,它将AFNetworking封装成抽象父类,而后根据每一种不一样的网络请求,都编写不一样的子类,子类继承父类,来实现请求业务。Moya在项目层次中的地位,有点相似于YTKNetwork。能够看下图对比 git
在阅读Moya源码以前,若是对POP有必定了解,那么理解其Moya会事半功倍的效果。在Objective-C也有协议,通常是让对象遵照协议,而后实现协议规定的方法,经过这种方式来对类实现扩展。POP其实就是把这种思路进一步强化。不少时候事物具有多样化的特质,而这些特质是没法单纯从一个类中继承而来的。为了解决这个痛点,C++有了多继承,即一个子类能够继承多种父类,这些被继承的父类之间不必定有关联。可是这依然会有其余问题,好比子类继承父类后,不必定须要用到全部的父类方法和属性,等于子类拥有了一些毫无用处的属性和方法。好比父类进行了修改,那么很难避免影响到子类。C++的多继承还会带来菱形缺陷
,什么是菱形缺陷
?本节的下方我会放两个连接,方便你们查阅。而Swift则引入了面向协议编程,经过协议来规定事物的实现。经过遵照不一样的协议,来对一个类或者结构体或者枚举进行定制,它只须要实现协议所规定的属性或方法便可,有点相似于搭建积木,取每一块有需求的模块,进行组合拼接,相对于OOP,其耦合性更低,也为代码的维护和拓展提供更多的可能性。关于POP思想大体是这样,下面是王巍关于POP的两篇文章,值得读一番。 面向协议编程与 Cocoa 的邂逅 (上) 面向协议编程与 Cocoa 的邂逅 (下)github
因为Moya是使用POP来设计的一个网络抽象层,所以他总体的逻辑结构并无明显的继承关系。Moya的核心代码,能够分红如下几个模块 编程
provider是一个提供网络请求服务的提供者。经过一些初始化配置以后,在外部能够直接用provider来发起request。json
在使用Moya进行网络请求时,第一步须要进行配置,来生成一个Request。首先按照官方文档,建立一个枚举,遵照TargetType协议,并实现协议所规定的属性。为何要建立枚举来遵照协议,而不像Objective-C那样建立类来遵照协议呢?其实使用类或者结构体也是能够的,这里猜想使用枚举的缘由是由于swift的枚举功能比Objective-C强大许多,枚举结合switch语句,使得API管理起来比较方便。 Request的生成过程以下图 swift
baseURL
path
method
sampleData
task
headers
复制代码
提供了这些网络请求的“基本材料”以后,就能够进一步配置去生成所须要的请求。看上图的第一个箭头,经过了一个EndpointClosure生成了endPoint。endPoit是一个对象,把网络请求所需的一些属性和方法进行了包装,在EndPoint类中有以下属性:api
public typealias SampleResponseClosure = () -> EndpointSampleResponse
open let url: String
open let sampleResponseClosure: SampleResponseClosure
open let method: Moya.Method
open let task: Task
open let httpHeaderFields: [String: String]?
复制代码
能够很直观地看出来,EndPoint这几个属性能够和上面经过TargetTpye配置的变量对应起来。那么这个过程在代码中作了哪些事? 在MoyaProvider类里,有以下声明数组
/// Closure that defines the endpoints for the provider.
public typealias EndpointClosure = (Target) -> Endpoint<Target>
open let endpointClosure: EndpointClosure
复制代码
声明了一个闭包,参数为Target,它是一个泛型,而后返回一个EndPoint。endPoint是一个类,它对请求的参数和动做进行了包装,下面会对它进行详细说明,先继续看endpointClosure作了什么。bash
endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping
复制代码
在MoyaProvider的初始化方法里,调用其扩展的类方法defaultEndpointMapping
输入Target做为参数,返回了一个endPoint对象。网络
public final class func defaultEndpointMapping(for target: Target) -> Endpoint<Target> {
return Endpoint(
url: URL(target: target).absoluteString,
sampleResponseClosure: { .networkResponse(200, target.sampleData) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers
)
}
复制代码
Target就是一开始进行配置的枚举,经过点语法取出Target的变量,完成endPoint的初始化。这里可能对于url和sampleResponseClosure会感到一些疑惑。url初始化,能够进入URL+Moya.swift
查看,它对NSURL类进行构造器的扩展,让其具有根据Moya的TargetType来进行初始化的能力。闭包
/// Initialize URL from Moya's `TargetType`. init<T: TargetType>(target: T) { // When a TargetType's path is empty, URL.appendingPathComponent may introduce trailing /, which may not be wanted in some cases
if target.path.isEmpty {
self = target.baseURL
} else {
self = target.baseURL.appendingPathComponent(target.path)
}
}
复制代码
sampleResponseClosure是一个和网络请求返回假数据相关的闭包,这里能够先忽略,不影响对Moya生成Request过程的理解。 咱们知道了MoyaProvider.defaultEndpointMapping能够返回endPoint对象后,从新看一遍这句
endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping
复制代码
使用@escaping把endpointClosure声明为逃逸闭包,咱们能够把
EndpointClosure = MoyaProvider.defaultEndpointMapping
复制代码
转换为
(Target) -> Endpoint<Target> = func defaultEndpointMapping(for target: Target) -> Endpoint<Target>
复制代码
再进一步转换,等号左边的能够写成一个常规的闭包表达式
{(Target)->Endpoint<Target> in
return Endpoint(
url: URL(target: target).absoluteString,
sampleResponseClosure: { .networkResponse(200, target.sampleData) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers
)
}
复制代码
即endpointClosure这个闭包,传入了Target做为参数,该闭包能够返回一个endPoint对象,如何获取到闭包返回的endPoint对象?MoyaProvider提供了这么一个方法
/// Returns an `Endpoint` based on the token, method, and parameters by invoking the `endpointClosure`.
open func endpoint(_ token: Target) -> Endpoint<Target> {
return endpointClosure(token)
}
复制代码
以上就是关于TargetType经过endpointClosure转化为endPoint的过程。
下一步就是把利用requestClosure,传入endPoint,而后生成request。request生成过程和endPoint很类似。
在MoyaProvider中声明:
/// Closure that decides if and what request should be performed
public typealias RequestResultClosure = (Result<URLRequest, MoyaError>) -> Void
open let requestClosure: RequestClosure
复制代码
而后在MoyaProvider的初始化方法里有很类似的一句
requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping,
复制代码
进入查看defaultRequestMapping方法
public final class func defaultRequestMapping(for endpoint: Endpoint<Target>, closure: RequestResultClosure) {
do {
let urlRequest = try endpoint.urlRequest()
closure(.success(urlRequest))
} catch MoyaError.requestMapping(let url) {
closure(.failure(MoyaError.requestMapping(url)))
} catch MoyaError.parameterEncoding(let error) {
closure(.failure(MoyaError.parameterEncoding(error)))
} catch {
closure(.failure(MoyaError.underlying(error, nil)))
}
}
复制代码
和endpointClosure相似,咱们通过转换,能够获得requestClosure的表达式为
{(endpoint:Endpoint<Target>, closure:RequestResultClosure) in
do {
let urlRequest = try endpoint.urlRequest()
closure(.success(urlRequest))
} catch MoyaError.requestMapping(let url) {
closure(.failure(MoyaError.requestMapping(url)))
} catch MoyaError.parameterEncoding(let error) {
closure(.failure(MoyaError.parameterEncoding(error)))
} catch {
closure(.failure(MoyaError.underlying(error, nil)))
}
}
复制代码
总体上使用do-catch语句来初始化一个urlRequest,根据不一样结果向闭包传入不一样的参数。一开始使用try来调用endpoint.urlRequest(),若是抛出错误,会切换到catch语句中去。endpoint.urlRequest()这个方法比较长,这里就不放出来,感兴趣可自行到Moya核心代码里的Endpoint.swift里查看。它其实作的事情很简单,就是根据前面说到的endpoint的那些属性来初始化一个NSURLRequest的对象。
以上就是上方图中所画的,根据TargetType最终生成Request的过程。不少人会感到疑惑,为何搞得这么麻烦,直接一步到位,传一些必要参数生成Request不就完了?为何还要再增长endPoint这么一个节点?根据Endpoint类所提供的一些方法来看,我的认为应该是为了更灵活地配置网络请求,以适应更多样化的业务需求。Endpoint类还有几个方法
/// Convenience method for creating a new `Endpoint` with the same properties as the receiver, but with added HTTP header fields.
open func adding(newHTTPHeaderFields: [String: String]) -> Endpoint<Target>
/// Convenience method for creating a new `Endpoint` with the same properties as the receiver, but with replaced `task` parameter.
open func replacing(task: Task) -> Endpoint<Target>
复制代码
借用这些方法,在endpointClosure中能够给一些网络请求添加请求头,替换请求参数,让这些请求配置更加灵活。
咱们看完了整个Request生成过程,那么经过requestClosure生成的的Request是如何被外部拿到的呢?这就是咱们下一步要探讨的,Provider发送请求实现过程。在下一节里将会看到如何使用这个Request。
咱们再来看一下官方文档里说明的Moya的基本使用步骤
provider = MoyaProvider<Myservice>()
其中第一步咱们在上方已经说明完了,MoyaProvider的初始化咱们只说明了一小部分。在此不许备一口气初始化方法中剩余的部分讲完,这又会涉及不少东西,同时理解起来会比较麻烦。在后面的代码解读中,若是有涉及到相关属性,再回到初始化方法中一个一个突破。
open func request(_ target: Target,
callbackQueue: DispatchQueue? = .none,
progress: ProgressBlock? = .none,
completion: @escaping Completion) -> Cancellable {
let callbackQueue = callbackQueue ?? self.callbackQueue
return requestNormal(target, callbackQueue: callbackQueue, progress: progress, completion: completion)
}
复制代码
直接从这里可能看不出什么,再追溯到requestNormal
中去 这个方法内容比较长,其中一些插件相关的代码,和测试桩的代码,暂且跳过不作说明,暂时不懂他们并不会成为理解provider.request的阻碍,它们属于可选内容,而不是必须的。
let endpoint = self.endpoint(target)
复制代码
生成了endPoint对象,这个很好理解,前面已经作过说明。 查看performNetworking
闭包
if cancellableToken.isCancelled {
self.cancelCompletion(pluginsWithCompletion, target: target)
return
}
复制代码
若是取消请求,则调用取消完成的回调,并return,不在执行闭包内下面的语句。 在这个闭包里传入了参数(requestResult: Result<URLRequest, MoyaError>)
,这里用到了Result,想深刻了解,可自行研究,这里简单说一下Result是干什么的。Result使用枚举方式,提供一些运行处理的结果,以下,很容易能看懂它所表达的意思。
switch requestResult {
case .success(let urlRequest):
request = urlRequest
case .failure(let error):
pluginsWithCompletion(.failure(error))
return
}
复制代码
若是请求成功,会拿到URLRequest,若是失败,会使用插件去处理失败回调。
// Allow plugins to modify request
let preparedRequest = self.plugins.reduce(request) { $1.prepare($0, target: target) }
复制代码
使用插件对请求进行完善
cancellableToken.innerCancellable = self.performRequest(target, request: preparedRequest, callbackQueue: callbackQueue, progress: progress, completion: networkCompletion, endpoint: endpoint, stubBehavior: stubBehavior)
复制代码
这里的self.performRequest
就是进行实际的网络请求,内部代码比较多,可是思路很简单,使用Alamofire的SessionManager来发送请求。 配置完成后就能够调用requestClosure(endpoint, performNetworking)
,执行这个闭包获取到上方所说的Request,来执行具体的网络请求了。
在使用Alamofire发送请求时,定义了闭包来处理请求的响应。Response这个类对于请求结果,提供了一些加工方法,好比data转json,图片转换等。
Moya提供了一个插件协议PluginType
,协议里规定了几种方法,阐明了插件的应用区域。
/// Called to modify a request before sending
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest
/// Called immediately before a request is sent over the network (or stubbed).
func willSend(_ request: RequestType, target: TargetType)
/// Called after a response has been received, but before the MoyaProvider has invoked its completion handler.
func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType)
/// Called to modify a result before completion
func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>
复制代码
prepare
能够在请求以前对request进行修改。willSend
在请求发送以前的一瞬间调用,这个能够用来添加请求时转圈圈的ToastdidReceive
在接收到请求响应时,且MoyaProvider的completion handler以前调用。process
在completion handler以前调用,用来修改请求结果 能够经过如下图来直观地理解插件调用时机
public final class NetworkActivityPlugin: PluginType {
public typealias NetworkActivityClosure = (_ change: NetworkActivityChangeType, _ target: TargetType) -> Void
let networkActivityClosure: NetworkActivityClosure
public init(networkActivityClosure: @escaping NetworkActivityClosure) {
self.networkActivityClosure = networkActivityClosure
}
public func willSend(_ request: RequestType, target: TargetType) {
networkActivityClosure(.began, target)
}
public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
networkActivityClosure(.ended, target)
}
}
复制代码
插件内部结构很简单,除了自行定义的一些变量外,就是遵照PluginType
协议后,去实现协议规定的方法,在特定方法内作本身须要作的事。由于PluginType
它已经有一个协议扩展,把方法的默认实现都完成了,在具体插件内不必定须要实现全部的协议方法,仅根据须要实现特定方法便可。 写好插件以后,使用起来也比较简答,MoyaProvider的初始化方法中,有个形参plugins: [PluginType] = []
,把网络请求中须要用到的插件加入数组中。
Moya能够说是很是Swift式的一个框架,最大的优势是使用面向协议的思想,让使用者能以搭积木的方式配置本身的网络抽象层。提供了插件机制,在保持主干网络请求逻辑的前提下,让开发者根据自身业务需求,定制本身的插件,在合适的位置加入到网络请求的过程当中。