Moya 淺析


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 } } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

例如有一個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 "找不到數據" } } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

主要是實現了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) } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

我們初始化了一個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] = []) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  1. 首先我們發現的是3個Closure:endpointClosure、requestClosure、stubClosure。這3個Closure是讓我們定制請求和進行測試時用的。非常有用,后面細說。

  2. 然后是一個Manager,Manager是真正用來網絡請求的類,Moya自己並不提供Manager類,Moya只是對其他網絡請求類進行了簡單的橋接。這么做是為了讓調用方可以輕易地定制、更換網絡請求的庫。比如你不想用Alamofire,可以十分簡單的換成其他庫

  3. 最后是一個類型為PluginType的數組。Moya提供了一個插件機制,使我們可以建立自己的插件類來做一些額外的事情。比如寫Log,顯示“菊花”等。抽離出Plugin層的目的,就是讓Provider職責單一,滿足開閉原則。把和自己網絡無關的行為抽離。避免各種業務揉在一起不利於擴展。

先來看看第一個EndpointClosure

EndpointClosure

//Moya.swift public typealias EndpointClosure = Target -> Endpoint<Target>
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

EndpointClosure這個閉包,輸入是一個Target,返回Endpoint。這就是我們前面說的Target -> Endpoint的轉換,那么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 ... }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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) }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

上面的代碼只是單純地創建並返回一個Endpoint實例。然而在很多時候,我們需要自定義這個閉包來做更多額外的事情。后面在stub小節,你會看到,我們用stub模擬API請求失敗的場景,給客戶端返回一個非200的狀態碼。為了實現這個功能,在這個閉包里處理相關的邏輯,再合適不過了!或者說這個閉包就是讓我們根據業務需求定制網絡請求的。

RequestClosure

//Moya.swift public typealias RequestClosure = (Endpoint<Target>, NSURLRequest -> Void) -> Void
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

RequestClosure這個閉包就是實現將Endpoint -> NSURLRequest,Moya也提供了一個默認實現:

//Moya.swift public final class func DefaultRequestMapping(endpoint: Endpoint<Target>, closure: NSURLRequest -> Void) { return closure(endpoint.urlRequest) }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

默認實現也只是簡單地調用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) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

從上面可以清晰地看出,EndpointClosure 和 RequestClosure 實現了 Target -> Endpoint -> NSRequest的轉換流

StubClosure

//Moya.swift public typealias StubClosure = Target -> Moya.StubBehavior
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

StubClosure這個閉包比較簡單,返回一個StubBehavior的枚舉值。它就是讓你告訴Moya你是否使用Stub返回數據或者怎樣使用Stub返回數據

//Moya.swift public enum StubBehavior { case Never //不使用Stub返回數據 case Immediate //立即使用Stub返回數據 case Delayed(seconds: NSTimeInterval) //一段時間間隔后使用Stub返回的數據 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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 = 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) } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

就這樣我們就實現了一個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) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

之后我們APP中所有的網絡請求,都會去經過我們MyHttpProxy的代理類。 
然而Moya的Stub不是這樣的,Moya的Stub的實現原理也超級無敵簡單!它不是系統級別的,非入侵式的。它只是簡單的加了一個判斷而已!還是在Moya的Request方法里面

//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 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

Moya先調用我們在構造函數中傳入的stubClosure閉包,如果stubBehavior是Never就真正的發起網絡請求,否 
者就調用self.stubRequest

//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.") } ... }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

如果Immediate,就馬上調用stub返回,是Delayed的話就Dispatch after延遲調用。

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 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

然后讓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 { }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

上面幾步,就完成了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)
        ... } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

如果你想自定義你自己的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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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) }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

協議里只有兩個方法,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 } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

插件實現代碼

插件的源碼實現也超級簡單。在進行網絡請求之前和收到請求后,遍歷所有的插件,調用其相關的接口。只是要分別處理下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)) } } } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

總結


總的來說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) //正確. 只能改名 } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我個人覺得這樣做,似乎並沒有重載簡潔。相比修改名字,我更喜歡重載。

  2. Target碎片化,后期維護困難 
    隨着業務的增加,Target會變得很復雜。TargetType協議它是利用多個屬性:method屬性、parameters屬性等。將一次API請求的實現的分割到多個了函數(屬性)中去實現。這就導致實現碎片化了。添加一個API請求,你需要修改幾個函數(屬性), 改幾個switch語句。如果文件很長,修改起來真的很煩,根本不好歸類整理。

  3. 不利於多人協作開發 
    因為大家每次添加新功能,修改的都是這幾個相同的函數(屬性),所以非常容易導致文件沖突。

 
 
 
 
 
 
 

Endpoints

Endpoint是一種半私有的數據結構,Moya用來解釋網絡請求的根本構成。一個endpoint儲存了以下數據:

  • The URL.
  • The HTTP method (GET,POST,等).
  • The request parameters.
  • The parameter encoding (URL,JSON,自定義,等).
  • The HTTP request header fields.
  • The sample response (單元測試用).

Providers 將 Targets 映射為Endpoints,然后將Endpoints映射為實際的網絡請求。

有兩種方式使用Endpoints。

  1. 創建一個provider的時候,可以定義一個Target到Endpoint的映射。
  2. 創建一個provider的時候,可以定義一個Endpoint到 NSURLRequest的映射。

第一種方式如下:

let endpointClosure = { (target: MyTarget) -> Endpoint<MyTarget> in let url = target.baseURL.URLByAppendingPathComponent(target.path).absoluteString return Endpoint(URL: url, sampleResponseClosure: {.NetworkResponse(200, target.sampleData)}, method: target.method, parameters: target.parameters) }

這其實是Moya provides的默認實現。如果你需要自定義,比如你的API需要自定義參數mapping,或者在單元測試中創建一個返回非200 HTTP statuses的測試provider,可以在這里實現。

第二種方式很少使用。Moya希望使用者盡量不用關注底層實現的細節。但如果你需要, 請接着往下看。

讓我們看看一個從Target到Endpoint的可變映射的示例。

From Target to Endpoint

默認情況,Endpoint 實例使用 .URL 類型的參數編碼。如果需要其他編碼方式,可以在配置provider時,用 Endpoint 的可選參數 parameterEncoding 來初始化你的endpointClosure

這里有四種編碼類型:.URL.JSON.PropertyList 和.Custom,可以直接解析成Alamofire可用的類型。這些也可以在provider的 endpointClosure 中配置。通常你只會用到 .URL,但也可以使用任何你需要的。這是直接解析為 Alamofire parameter encodings

你可以在閉包里為HTTP頭文件添加參數。例如,我們可能想要在HTTP頭文件中設置"APP_NAME"以便服務器端解析。

let endpointClosure = { (target: MyTarget) -> Endpoint<MyTarget> in let url = target.baseURL.URLByAppendingPathComponent(target.path).absoluteString let endpoint: Endpoint<MyTarget> = Endpoint<MyTarget>(URL: url, sampleResponseClosure: {.NetworkResponse(200, target.sampleData)}, method: target.method, parameters: target.parameters) return endpoint.endpointByAddingHTTPHeaderFields(["APP_NAME": "MY_AWESOME_APP"]) }

這也意味着你可以為部分或全部endpoints提供附加參數。例如,我們需要給所有MyTarget 類型的 target添加認證token,但不包括用來進行認證的target。這就需要構造一個如下的 endpointClosure

let endpointClosure = { (target: MyTarget) -> Endpoint<MyTarget> in let url = target.baseURL.URLByAppendingPathComponent(target.path).absoluteString let endpoint: Endpoint<MyTarget> = Endpoint<MyTarget>(URL: url, sampleResponseClosure: {.NetworkResponse(200, target.sampleData)}, method: target.method, parameters: target.parameters) // Sign all non-authenticating requests switch target { case .Authenticate: return endpoint default: return endpoint.endpointByAddingHTTPHeaderFields(["AUTHENTICATION_TOKEN": GlobalAppStorage.authToken]) } }

注:我們可以在Moya現有的方法上進行擴展,endpointByAddingParameters 和 endpointByAddingHTTPHeaderFields 允許你利用Moya現有的代碼添加自定義value。

Sample responses是 TargetType 協議所必須的。然而,這只是定義了返回數據。Target-to-Endpoint映射閉包可以定義更多細節,在單元測試時非常有用。

Sample responses返回下面二者之一:

  • NetworkResponse,包含一個 Int 類型的status code 和NSData 類型的返回數據。
  • NetworkError,包含一個 NSError? 類型的error。

Request Mapping

我們最初就提到,這個庫不是一個封裝網絡請求的第三方庫 - 那是Alamofire干的事。事實上,Moya是一種封裝網絡訪問的方式,並提供編譯時檢查已經定義的targets。你已經知道怎樣在MoyaProvider 的初始化中用 endpointClosure 把targets映射為endpoints。Moya會根據你創建的 Endpoint 實例來推動API請求。某些情況,Endpoint 必須要解析為 NSURLRequest 提供給Alamofire,這也就是 requestClosure 的作用。

requestClosure 是可選的,是修改request的根本辦法。MoyaProvider.DefaultRequestMapper 使用 Endpoint 的 urlRequest 屬性作為默認值。

這個閉包接受一個 Endpoint 作為參數,以及一個NSURLRequest -> Void ,在這里可以完成OAuth認證或其他事情。你想異步的調用這個閉包的時候,也可以使用第三方庫認證 (example)。不需要修改request的話,可以單純的log。

let requestClosure = { (endpoint: Endpoint<GitHub>, done: NSURLRequest -> Void) in let request = endpoint.urlRequest // Modify the request however you like. done(request) } provider = MoyaProvider<GitHub>(requestClosure: requestClosure)

requestClosure 在修改 NSURLRequest 屬性時很好用,或者提供一些請求創建之前不知道的信息,例如cookies設置。請注意前面提到的 endpointClosure 不能實現這種應用層的特定請求。

這個屬性對於修改request對象非常有用,NSURLRequest 可以自定義很多屬性,例如你想讓禁用所有cookies:

{ (endpoint: Endpoint<ArtsyAPI>) -> (NSURLRequest) in let request: NSMutableURLRequest = endpoint.urlRequest.mutableCopy() as NSMutableURLRequest request.HTTPShouldHandleCookies = false return request }

你也在請求送達之前調用這個閉包來可以打印網絡請求。

 
 

RxSwift

Maya提供了一個可選的MoyaProvider 子類 - RxMoyaProvider。在網絡請求完成時,我們不再使用 request() 函數的回調閉包,而是使用 Observable

RxMoyaProvider 可以像 MoyaProvider 一樣創建和使用:

let provider = RxMoyaProvider<GitHub>()

然后,你就可以干很多事情:

provider.request(.Zen).subscribe { (event) -> Void in switch event { case .Next(let response): // do something with the data case .Error(let error): // handle the error default: break } }

對於 RxMoyaProvider,在請求被訂閱前,不會發起網絡請求。如果網絡請求完成之前,訂閱的信號被銷毀了,請求將被取消。

如果請求正常完成,將會發生兩件事:

  1. observable 發送一個 Moya.Response 類型的值 。
  2. observable 結束.

如果請求發生了錯誤(通常是NSURLSession錯誤),錯誤碼是網絡請求失敗的status code,如果有的話,和response data,如果也有的話。

Moya.Response 類型包含一個 statusCode ,一些 data,和一個可以為空的 NSURLResponse。不管你習慣 subscribeNext 還是 map ,都可以使用這些數據。

更棒的是,Moya提供了一些Observable 的擴展,讓你更簡單的處理 MoyaResponses

  • filterStatusCodes() 提供了一系列status code。如果返回的status code不在其中,將生成一個error。
  • filterStatusCode() 用於查找特殊的status cod,如果沒有找到,將生成error。
  • filterSuccessfulStatusCodes() 篩選200系列的status codes。
  • filterSuccessfulStatusAndRedirectCodes() 篩選200-300系列的status codes。
  • mapImage() 嘗試將返回數據轉換成 UIImage,失敗的話將生成error。
  • mapJSON() 嘗試將返回數據轉換成 JSON對象,失敗的話將生成error。
  • mapString()嘗試將返回數據轉換成字符串,失敗的話將生成error。

在網絡請求錯誤的情況下,error的 domain 是 MoyaErrorDomain。 通常code是 MoyaErrorCode 的rawValue。底層的errors提供了原始的返回數據,在 NSError 的 userInfo 中,關鍵字為"data"。

 
 
參考鏈接:
1.http://blog.csdn.net/lh844386434/article/details/51818017
2.http://www.cnblogs.com/liuliuliu/p/5627944.html
3.http://www.cnblogs.com/liuliuliu/p/5624026.html
4.http://www.cnblogs.com/liuliuliu/p/5626788.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM