Moya源码解析

来源:互联网 发布:editplus怎么用java 编辑:程序博客网 时间:2024/05/16 11:22

Moya是一个高度抽象的网络库,他的理念是让你不用关心网络请求的底层的实现细节,只用定义你关心的业务。且Moya采用桥接和组合来进行封装(默认桥接了Alamofire),使得Moya非常好扩展,让你不用修改Moya源码就可以轻易定制。官方给出几个Moya主要优点:

  • 编译时检查API endpoint权限
  • 让你使用枚举定义各种不同Target, endpoints
  • 把stubs当做一等公民对待,因此测试超级简单。

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(不合理), 但 仅仅是做一个例子而已。

Providers

Providers是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.swiftpublic init(endpointClosure: EndpointClosure = MoyaProvider.DefaultEndpointMapping,        requestClosure: RequestClosure = MoyaProvider.DefaultRequestMapping,           stubClosure: StubClosure = MoyaProvider.NeverStub,        manager: Manager = MoyaProvider<Target>.DefaultAlamofireManager(),        plugins: [PluginType] = []) 
  1. 首先我们发现的是3个Closure:endpointClosure、requestClosure、stubClosure。这3个Closure是让我们定制请求和进行测试时用的。非常有用,后面细说。

  2. 然后是一个Manager,Manager是真正用来网络请求的类,Moya自己并不提供Manager类,Moya只是对其他网络请求类进行了简单的桥接。这么做是为了让调用方可以轻易地定制、更换网络请求的库。比如你不想用Alamofire,可以十分简单的换成其他库

  3. 最后是一个类型为PluginType的数组。Moya提供了一个插件机制,使我们可以建立自己的插件类来做一些额外的事情。比如写Log,显示“菊花”等。抽离出Plugin层的目的,就是让Provider职责单一,满足开闭原则。把和自己网络无关的行为抽离。避免各种业务揉在一起不利于扩展。

先来看看第一个EndpointClosure

EndpointClosure

//Moya.swiftpublic typealias EndpointClosure = Target -> Endpoint<Target>

EndpointClosure这个闭包,输入是一个Target,返回Endpoint。这就是我们前面说的Target -> Endpoint的转换,那么Endpoint是个什么鬼?
Endpoint 是Moya最终进行网络请求前的一种数据结构,它保存了这些数据:

  • URL
  • HTTP请求方式 (GET, POST, etc).
  • 本次请求的参数
  • 参数的编码方式 (URL, JSON, custom, etc).
  • stub数据的 response(测试用的)
//Endpoint.swiftpublic 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.swiftpublic 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.swiftpublic typealias RequestClosure = (Endpoint<Target>, NSURLRequest -> Void) -> Void

RequestClosure这个闭包就是实现将Endpoint -> NSURLRequest,Moya也提供了一个默认实现:

//Moya.swiftpublic 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)

从上面可以清晰地看出,EndpointClosureRequestClosure 实现了 Target -> Endpoint -> NSRequest的转换流

StubClosure

//Moya.swiftpublic typealias StubClosure = Target -> Moya.StubBehavior

StubClosure这个闭包比较简单,返回一个StubBehavior的枚举值。它就是让你告诉Moya你是否使用Stub返回数据或者怎样使用Stub返回数据

//Moya.swiftpublic 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, target.sampleData)}, method: target.method, 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 = loginAPIProviderloginAPIProvider.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中Provider对象在销毁的时候会去Cancel网络请求。为了得到正确的结果,你必须保证在网络请求的时候你的Provider不会被释放。否者你会得到下面的错误 “But don’t forget to keep a reference for it in property. If it gets deallocated you’ll see -999 “cancelled” error on response” 。通常为了避免这种情况,你可以将Provider实例设置为类成员变量,或者shared实例

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的Stub不是这样的,Moya的Stub的实现原理也超级无敌简单!它不是系统级别的,非入侵式的。它只是简单的加了一个判断而已!还是在Moya的Request方法里面

//Moya.swiftpublic 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, request: request, completion: completion, endpoint: endpoint, stubBehavior: stubBehavior)            }        }        requestClosure(endpoint, performNetworking)        return cancellableToken    }

Moya先调用我们在构造函数中传入的stubClosure闭包,如果stubBehavior是Never就真正的发起网络请求,否
者就调用self.stubRequest

//Moya.swiftinternal func stubRequest(target: Target, request: NSURLRequest, completion: Moya.Completion, endpoint: Endpoint<Target>, stubBehavior: Moya.StubBehavior) -> CancellableToken {        ...        let stub: () -> () = createStubFunction(cancellableToken, forTarget: target, withCompletion: completion, endpoint: endpoint, 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)
为了达到这个目的Moya做了几件事情:

首先抽象了一个RequestType协议,利用这个协议将Alamofire隐藏了起来,让Provider类依赖于这个协议,而不是具体细节。

//Plugin.swiftpublic 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.swiftpublic 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.swiftfunc sendRequest(target: Target, request: NSURLRequest, 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)

Plugin

Moya提供还提供插件机制,你可以自定义各种插件,所有插件必须满足PluginType协议

//Plugin.swiftpublic 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默认提供了三个插件:

  • Authentication插件 (CredentialsPlugin.swift)。 HTTP认证的插件。
  • Logging插件(NetworkLoggerPlugin.swift)。在调试是,输入网络请求的调试信息到控制台
  • Network Activity Indicator插件(NetworkActivityPlugin.swift)。可以用这个插件来显示网络菊花

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.swiftfunc sendRequest(target: Target, request: NSURLRequest, queue: dispatch_queue_t?, 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, endpoint: Endpoint<Target>, 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中去,也会导致另外一些问题:
(以下仅是个人观点,仅供参考)

  1. 枚举无法重载,代码未必简洁
    比如,现在要添加一个新接口,还是要求实现Login功能,除了支持已有的用户名/密码登录,还要支持指纹登录。那么我们想定义可能想这样:Login(fingerPrint: String)。这两种登录情况实际上只是参数不一样。但在因为枚举中不能重载,所以为了添加这个case,我们不得不重新取一个名字,而不能利用函数重载。

    enum AccountAPI {case Login(userName: String, passwd: String)case Register(userName: String, passwd: String)//case Login(fingerPrint: String) //error: 不能这样添加错的,不支持重载case LoginWithPrint(fingerPrint: String) //正确. 只能改名}

    我个人觉得这样做,似乎并没有重载简洁。相比修改名字,我更喜欢重载。

  2. Target碎片化,后期维护困难
    随着业务的增加,Target会变得很复杂。TargetType协议它是利用多个属性:method属性、parameters属性等。将一次API请求的实现的分割到多个了函数(属性)中去实现。这就导致实现碎片化了。添加一个API请求,你需要修改几个函数(属性), 改几个switch语句。如果文件很长,修改起来真的很烦,根本不好归类整理。

  3. 不利于多人协作开发
    因为大家每次添加新功能,修改的都是这几个相同的函数(属性),所以非常容易导致文件冲突。

0 0
原创粉丝点击