Moya

Moya

  • 什么是moya,moya是什么?

答:高度抽象的API,默认桥接Alamofire

Moya主要优点:

  • 编译时检查API endpoint权限

  • 让你使用枚举定义各种不同Target, endpoints

  • 把stubs当做一等公民对待,因此测试超级简单。

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模块,模块实现注册登录的功能


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': '666',Token':'55555'}".dataUsingEncoding(NSUTF8StringEncoding)!
        case .Register(let userName, let passwd):
            return "找不到数据"
        }
    }
}

  • Providers

let provider = MoyaProvider<AccountAPI>()
provider.request(.Login) { result in
    // `result` is either .Success(response) or .Failure(error)
}

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] = []) 

  • 3个Closure:

    • endpointClosure、requestClosure、stubClosure。这3个Closure是让我们定制请求,修改请求方式和进行测试时可以用到
  • Manager

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

    • PluginType是一个数组 Moya提供了一个插件机制,使我们可以建立自己的插件类来做一些额外的事情。比如写Log,显示“菊花”等。抽离出Plugin层的目的,就是让Provider职责单一,满足开闭原则。把和自己网络无关的行为抽离

EndpointClosure


//Moya.swift
public typealias EndpointClosure = Target -> Endpoint<Target>

  • EndpointClosure这个闭包,输入是一个Target,返回Endpoint。

    • Endpoint 是Moya最终进行网络请求前的一种数据结构,它保存了这些数据:
* URL
* HTTP请求方式 (GET, POST, etc).
* 本次请求的参数
* 参数的编码方式 (URL, JSON, custom, etc).
* stub数据的 response(测试用的)

//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提供一个DefaultEndpointMapping的函数,来实现这个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来测试

Moya 使用示例


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 = 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)
}

// 注意: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实例

  • stub 直接上源码

    • self.sendRequest 发送请求

    • self.stubRequest stub请求


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




//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, 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.")
        }
        ...
}

  • Manager
// Moya并不是一个网络请求的三方库,它只是一个抽象的网络层。它对其他网络库的进行了桥接,真正进行网络请求是别人的网络库(比如默认的Alamofire.Manager)为了达到这个目的Moya做了几件事情:

首先抽象了一个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+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, 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.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默认提供了三个插件:
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.swift
func 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))
            }
        }
    }

枚举无法重载,代码未必简洁
比如,现在要添加一个新接口,还是要求实现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) //正确. 只能改名
}

666 哈哈哈哈哈哈哈

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • Moya[https://github.com/Moya/Moya]是一个高度抽象的网络库,他的理念是让你不用关心...
    村雨灬龑阅读 458评论 0 0
  • Moya的使用 关于Moya Moya是对Alamofire的再次封装。 让我们用一张图来简单来对比一下直接用Al...
    chensx1993阅读 29,702评论 9 79
  • 文章摘自Moya官方文档 Targets Moya的使用始于定义一个target——典型的是定义一个符合Targe...
    Jt_Self阅读 16,126评论 0 27
  • Moya-Basic.md Basic Usage(基本用法) 如何来使用这个库呢?额,相当简单滴!只要跟随这个模...
    心印印心阅读 633评论 0 1
  • 原文地址:https://github.com/Moya/Moya/tree/master/docs参考文章:ht...
    狂奔的兔子阅读 3,324评论 0 53