Moya源码解析
Moya是一个高度抽象的网络库,他的理念是让你不用关心网络请求的底层的实现细节,只用定义你关心的业务。且Moya采用桥接和组合来进行封装(默认桥接了Alamofire),使得Moya非常好扩展,让你不用修改Moya源码就可以轻易定制。官方给出几个Moya主要优点:
Target开始Moya之旅的第一步便是,建立一个Enum的Target,这个Target便是你网络请求相关行为的定义。Target必须实现TargetType协议。 public protocol TargetType { var baseURL: NSURL { get }
var path: String { get }
var method: Moya.Method { get }
var parameters: [String: AnyObject]? { get }
var sampleData: NSData { get }
}
例如有一个AccountAPI模块,模块实现注册登录的功能。所以第一件事情,我们需要定义一个Target enum AccountAPI {
case Login(userName: String,passwd: String)
case Register(userName: String,passwd: String)
}
extension AccountAPI: TargetType {
var baseURL: NSURL {
return NSURL(string: "https://www.myapp.com")!
}
var path: String {
switch self {
case .Login:
return "/login"
case .Register:
return "/register"
}
}
var method: Moya.Method {
return .GET
}
var parameters: [String: AnyObject]? {
switch self {
case .Login:
return nil
case .Register(let userName,let passwd):
return ["username": userName,"password": passwd]
}
}
var sampleData: NSData {
switch self {
case .Login:
return "{'code': 1,6'Token':'123455'}".dataUsingEncoding(NSUTF8StringEncoding)!
case .Register(let userName,let passwd):
return "找不到数据"
}
}
}
主要是实现了TargetType协议,里面的网址和内容,是随便写的,可能不make sence(不合理),但 仅仅是做一个例子而已。 ProvidersProviders是Moya中的核心,Moya中所有的API请求都是通过Provider来发起的。因此大多数时候,你的代码请求像这样: let provider = MoyaProvider<AccountAPI>()
provider.request(.Login) { result in
// `result` is either .Success(response) or .Failure(error)
}
我们初始化了一个AccountAPI的Provider,并且调用了Login请求。怎么样?干净简单吧! 从Provider的构造函数说起Provider真正做的事情可以用一个流来表示:Target -> Endpoint -> Request 。在这个例子中,它将AccountAPI转换成Endpoint,再将其转换成为NSRURLRequest。最后将这个NSRURLRequest交给Alamofire去进行网络请求。 我们从Provider的构造函数开始切入,一步一步地扒开它。 //Moya.swift
public init(endpointClosure: EndpointClosure = MoyaProvider.DefaultEndpointMapping,requestClosure: RequestClosure = MoyaProvider.DefaultRequestMapping,stubClosure: StubClosure = MoyaProvider.NeverStub,manager: Manager = MoyaProvider<Target>.DefaultAlamofireManager(),plugins: [PluginType] = [])
先来看看第一个EndpointClosure EndpointClosure//Moya.swift
public typealias EndpointClosure = Target -> Endpoint<Target>
EndpointClosure这个闭包,输入是一个Target,返回Endpoint。这就是我们前面说的Target -> Endpoint的转换,那么Endpoint是个什么鬼?
//Endpoint.swift
public class Endpoint<Target> {
public typealias SampleResponseClosure = () -> EndpointSampleResponse
public let URL: String
public let method: Moya.Method
public let sampleResponseClosure: SampleResponseClosure
public let parameters: [String: AnyObject]?
public let parameterEncoding: Moya.ParameterEncoding
...
}
Moya提供一个默认EndpointClosure的函数,来实现这个Target到Endpoint的转换: //Moya.swift
public final class func DefaultEndpointMapping(target: Target) -> Endpoint<Target> { let url = target.baseURL.URLByAppendingPathComponent(target.path).absoluteString return Endpoint(URL: url,sampleResponseClosure: {.NetworkResponse(200,target.sampleData)},method: target.method,parameters: target.parameters)
}
上面的代码只是单纯地创建并返回一个Endpoint实例。然而在很多时候,我们需要自定义这个闭包来做更多额外的事情。后面在stub小节,你会看到,我们用stub模拟API请求失败的场景,给客户端返回一个非200的状态码。为了实现这个功能,在这个闭包里处理相关的逻辑,再合适不过了!或者说这个闭包就是让我们根据业务需求定制网络请求的。 RequestClosure//Moya.swift
public typealias RequestClosure = (Endpoint<Target>,NSURLRequest -> Void) -> Void
RequestClosure这个闭包就是实现将Endpoint -> NSURLRequest,Moya也提供了一个默认实现: //Moya.swift
public final class func DefaultRequestMapping(endpoint: Endpoint<Target>,closure: NSURLRequest -> Void) {
return closure(endpoint.urlRequest)
}
默认实现也只是简单地调用endpoint.urlRequest取得一个NSURLRequest实例。然后调用了closure。然而,你可以在这里修改这个请求Request,事实上这也是Moya给你的最后的机会。举个例子,你想禁用所有的cookie,并且设置超时时间等。那么你可以实现这样的闭包: let requestClosure = { (endpoint: Endpoint<GitHub>,done: NSURLRequest -> Void) in
//可以在这里修改request
let request: NSMutableURLRequest = endpoint.urlRequest.mutableCopy() as NSMutableURLRequest
request.HTTPShouldHandleCookies = false
request.timeoutInterval = 20
done(request)
}
provider = MoyaProvider(requestClosure: requestClosure)
从上面可以清晰地看出,EndpointClosure 和 RequestClosure 实现了 Target -> Endpoint -> NSRequest的转换流 StubClosure//Moya.swift
public typealias StubClosure = Target -> Moya.StubBehavior
StubClosure这个闭包比较简单,返回一个StubBehavior的枚举值。它就是让你告诉Moya你是否使用Stub返回数据或者怎样使用Stub返回数据 //Moya.swift
public enum StubBehavior {
case Never //不使用Stub返回数据
case Immediate //立即使用Stub返回数据
case Delayed(seconds: NSTimeInterval) //一段时间间隔后使用Stub返回的数据
}
Never表明不使用Stub来返回模拟的网络数据, Immediate表示马上返回Stub的数据, Delayed是在几秒后返回。Moya默认是不使用Stub来测试。 在Target那一节我们定义了一个AccountAPI,API中我们实现了接口sampleData,这个属性是返回Stub数据的。 extension AccountAPI: TargetType {
...
var sampleData: NSData {
switch self {
case .Login:
return "{'code': 1,6'Token':'123455'}".dataUsingEncoding(NSUTF8StringEncoding)!
case .Register(let userName,let passwd):
return "找不到数据"
}
}
}
let endPointAction = { (target: TargetType) -> Endpoint<AccountAPI> in
let url = target.baseURL.URLByAppendingPathComponent(target.path).absoluteString
switch target {
case .Login:
return Endpoint(URL: url,sampleResponseClosure: {.NetworkResponse(200,target.sampleData)},method: target.method,parameters: target.parameters)
case .Register:
return Endpoint(URL: url,sampleResponseClosure: {.NetworkResponse(404,parameters: target.parameters)
}
}
let stubAction: (type: AccountAPI) -> Moya.StubBehavior = { type in
switch type {
case .Login:
return Moya.StubBehavior.Immediate
case .Register:
return Moya.StubBehavior.Delayed(seconds: 3)
}
}
let loginAPIProvider = MoyaProvider<AccountAPI>(
endpointClosure: endPointAction,stubClosure: stubAction
)
self.netProvider = loginAPIProvider
loginAPIProvider.request(AccountAPI.Login(userName: "user",passwd: "123456")) { (result) in
switch result {
case .Success(let respones) :
print(respones)
case .Failure(_) :
print("We got an error")
}
print(result)
}
就这样我们就实现了一个Stub! Login和Register都使用了Stub返回的数据。
Moya中Stub的实现大多iOS的Http的Stub框架本质都是实现一个HTTP网络请求的代理类,去Hook系统Http请求。 如OHHTTPStub就是这么做的。在iOS中,HTTP代理类需要继承NSURLProtocol类,重载一些父类的方法,然后将这个代理类注册到系统中去。 class MyHttpProxy : NSURLProtocol {
//重载一些父类的方法
override class func canInitWithRequest(request: NSURLRequest) -> Bool {
return true
}
override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {
return super.canonicalRequestForRequest(request)
}
....
}
//注册
NSURLProtocol.registerClass(MyHttpProxy.self)
之后我们APP中所有的网络请求,都会去经过我们MyHttpProxy的代理类。 //Moya.swift
public func request(target: Target,queue:dispatch_queue_t?,completion: Moya.Completion) -> Cancellable {
let endpoint = self.endpoint(target)
let stubBehavior = self.stubClosure(target)
var cancellableToken = CancellableWrapper()
let performNetworking = { (request: NSURLRequest) in
if cancellableToken.isCancelled { return }
switch stubBehavior {
case .Never:
cancellableToken.innerCancellable = self.sendRequest(target,request: request,queue: queue,completion: completion)
default:
cancellableToken.innerCancellable = self.stubRequest(target,completion: completion,endpoint: endpoint,stubBehavior: stubBehavior)
}
}
requestClosure(endpoint,performNetworking)
return cancellableToken
}
Moya先调用我们在构造函数中传入的stubClosure闭包,如果stubBehavior是Never就真正的发起网络请求,否 //Moya.swift
internal func stubRequest(target: Target,request: NSURLRequest,completion: Moya.Completion,endpoint: Endpoint<Target>,stubBehavior: Moya.StubBehavior) -> CancellableToken {
...
let stub: () -> () = createStubFunction(cancellableToken,forTarget: target,withCompletion: completion,plugins: plugins)
switch stubBehavior {
case .Immediate:
stub()
case .Delayed(let delay):
let killTimeOffset = Int64(CDouble(delay) * CDouble(NSEC_PER_SEC))
let killTime = dispatch_time(DISPATCH_TIME_NOW,killTimeOffset)
dispatch_after(killTime,dispatch_get_main_queue()) {
stub()
}
case .Never:
fatalError("Method called to stub request when stubbing is disabled.")
}
...
}
如果Immediate,就马上调用stub返回,是Delayed的话就Dispatch after延迟调用。 Manager我们知道,Moya并不是一个网络请求的三方库,它只是一个抽象的网络层。它对其他网络库的进行了桥接,真正进行网络请求是别人的网络库(比如默认的Alamofire.Manager) 首先抽象了一个RequestType协议,利用这个协议将Alamofire隐藏了起来,让Provider类依赖于这个协议,而不是具体细节。 //Plugin.swift
public protocol RequestType {
var request: NSURLRequest? { get }
func authenticate(user user: String,password: String,persistence: NSURLCredentialPersistence) -> Self
func authenticate(usingCredential credential: NSURLCredential) -> Self
}
然后让Moya.Manager == Alamofire.Manager,并且让Alamofire.Manager也实现RequestType协议 Moya+Alamofire.swift
public typealias Manager = Alamofire.Manager
/// Choice of parameter encoding.
public typealias ParameterEncoding = Alamofire.ParameterEncoding
//让Alamofire.Manager也实现 RequestType协议
extension Request: RequestType { }
上面几步,就完成了Alamofire的封装、桥接。正因为桥接封装了Alamofire,因此Moya的request,最终一定会调用Alamofire的request。简单的跟踪下Moya的Request方法就可以发现sendRequest调用了Alamofire。 //Moya.swift
func sendRequest(target: Target,queue: dispatch_queue_t?,completion: Moya.Completion) -> CancellableToken {
//调用Alamofire发起网络请求
let alamoRequest = manager.request(request)
...
}
如果你想自定义你自己的Manager,你可以传入你自己的Manager到Privoder。之后所有的请求都会经过你的这个Manager let policies: [String: ServerTrustPolicy] = [
"example.com": .PinPublicKeys(
publicKeys: ServerTrustPolicy.publicKeysInBundle(),validateCertificateChain: true,validateHost: true
)
]
let manager = Manager(
configuration: NSURLSessionConfiguration.defaultSessionConfiguration(),serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies)
)
let provider = MoyaProvider<MyTarget>(manager: manager)
PluginMoya提供还提供插件机制,你可以自定义各种插件,所有插件必须满足PluginType协议 //Plugin.swift
public protocol PluginType {
/// Called immediately before a request is sent over the network (or stubbed).
func willSendRequest(request: RequestType,target: TargetType)
// Called after a response has been received,but before the MoyaProvider has invoked its completion handler.
func didReceiveResponse(result: Result<Moya.Response,Moya.Error>,target: TargetType)
}
协议里只有两个方法,willSendRequest和didReceiveResponse。在进行网络请求之前和收到请求后,Moya会遍历所有的插件。分别去调用插件各自的willSendRequest和didReceiveResponse方法。 个人觉得这个插件更像是一个网络回调的Delegate,只是取了一个高大上的名字而已。不过将网络回调抽取出来确实能更好地将无关业务隔离,让Privoder更加专心的做自己的事情。而且以后也非常好扩展。 Moya默认提供了三个插件:
Network Activity Indicator插件用法示例,在网络进行请求开始请求时添加一个Spinner,请求结束隐藏Spinner。这里用的是SwiftSpinner let spinerPlugin = NetworkActivityPlugin { state in
if state == .Began {
SwiftSpinner.show("Connecting...")
} else {
SwiftSpinner.show("request finish...")
SwiftSpinner.hide()
}
let loginAPIProvider = MoyaProvider<AccountAPI>(
plugins: [spinerPlugin]
)
loginAPIProvider.request(.Login) { _ in }
插件实现代码 插件的源码实现也超级简单。在进行网络请求之前和收到请求后,遍历所有的插件,调用其相关的接口。只是要分别处理下Stub和真正进行网络请求的两种情况 //Moya.swift
func sendRequest(target: Target,completion: Moya.Completion) -> CancellableToken {
let alamoRequest = manager.request(request)
let plugins = self.plugins
// 遍历插件,通知开始请求
plugins.forEach { $0.willSendRequest(alamoRequest,target: target) }
// Perform the actual request
alamoRequest.response(queue: queue) { (_,response: NSHTTPURLResponse?,data: NSData?,error: NSError?) -> () in
let result = convertResponseToResult(response,data: data,error: error)
// 遍历插件,通知收到请求
plugins.forEach { $0.didReceiveResponse(result,target: target) }
completion(result: result)
}
alamoRequest.resume()
return CancellableToken(request: alamoRequest)
}
//在测试时,Stub分支的也要,遍历调用一次插件
internal final func createStubFunction(token: CancellableToken,forTarget target: Target,withCompletion completion: Moya.Completion,plugins: [PluginType]) -> (() -> ()) { return { if (token.canceled) { let error = Moya.Error.Underlying(NSError(domain: NSURLErrorDomain,code: NSURLErrorCancelled,userInfo: nil)) //调用插件 plugins.forEach { $0.didReceiveResponse(.Failure(error),target: target) } completion(result: .Failure(error)) return } switch endpoint.sampleResponseClosure() { case .NetworkResponse(let statusCode,let data): let response = Moya.Response(statusCode: statusCode,data: data,response: nil) //成功情况,调用插件 plugins.forEach { $0.didReceiveResponse(.Success(response),target: target) } completion(result: .Success(response)) case .NetworkError(let error): let error = Moya.Error.Underlying(error) //失败情况,调用插件 plugins.forEach { $0.didReceiveResponse(.Failure(error),target: target) } completion(result: .Failure(error)) } } }
总结总的来说Moya的实现比较简单,但是基于作者这种桥接、封装的思路,使得Moya扩展十分灵活,所以Moya有各种Provider,能和RxSwift,RAC等等轻松的结合。 而Moya用起来也非常的干净。你不用关心Request具体实现。只用专注于你自己的Target设计就行。再加上Moya的Stub特性,的确使得它十分易于测试。 自己的思考成也萧何败也萧何。然而我自己的感受,Moya让我们把所有的业务都放到Target中去,也会导致另外一些问题:
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |