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
首先我们发现的是3个Closure:endpointClosure、requestClosure、stubClosure。这3个Closure是让我们定制请求和进行测试时用的。非常有用,后面细说。
然后是一个Manager,Manager是真正用来网络请求的类,Moya自己并不提供Manager类,Moya只是对其他网络请求类进行了简单的桥接。这么做是为了让调用方可以轻易地定制、更换网络请求的库。比如你不想用Alamofire,可以十分简单的换成其他库
最后是一个类型为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中去,也会导致另外一些问题:
(以下仅是个人观点,仅供参考)
枚举无法重载,代码未必简洁
比如,现在要添加一个新接口,还是要求实现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
我个人觉得这样做,似乎并没有重载简洁。相比修改名字,我更喜欢重载。
Target碎片化,后期维护困难
随着业务的增加,Target会变得很复杂。TargetType协议它是利用多个属性:method属性、parameters属性等。将一次API请求的实现的分割到多个了函数(属性)中去实现。这就导致实现碎片化了。添加一个API请求,你需要修改几个函数(属性), 改几个switch语句。如果文件很长,修改起来真的很烦,根本不好归类整理。不利于多人协作开发
因为大家每次添加新功能,修改的都是这几个相同的函数(属性),所以非常容易导致文件冲突。
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。
- 创建一个provider的时候,可以定义一个Target到Endpoint的映射。
- 创建一个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
,在请求被订阅前,不会发起网络请求。如果网络请求完成之前,订阅的信号被销毁了,请求将被取消。
如果请求正常完成,将会发生两件事:
- observable 发送一个
Moya.Response
类型的值 。 - 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"。
Moya 浅析的更多相关文章
- SQL Server on Linux 理由浅析
SQL Server on Linux 理由浅析 今天的爆炸性新闻<SQL Server on Linux>基本上在各大科技媒体上刷屏了 大家看到这个新闻都觉得非常震精,而美股,今天微软开 ...
- 【深入浅出jQuery】源码浅析--整体架构
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- 高性能IO模型浅析
高性能IO模型浅析 服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种: (1)同步阻塞IO(Blocking IO):即传统的IO模型. (2)同步非阻塞IO(Non-blocking ...
- netty5 HTTP协议栈浅析与实践
一.说在前面的话 前段时间,工作上需要做一个针对视频质量的统计分析系统,各端(PC端.移动端和 WEB端)将视频质量数据放在一个 HTTP 请求中上报到服务器,服务器对数据进行解析.分拣后从不同的 ...
- Jvm 内存浅析 及 GC个人学习总结
从诞生至今,20多年过去,Java至今仍是使用最为广泛的语言.这仰赖于Java提供的各种技术和特性,让开发人员能优雅的编写高效的程序.今天我们就来说说Java的一项基本但非常重要的技术内存管理 了解C ...
- 从源码浅析MVC的MvcRouteHandler、MvcHandler和MvcHttpHandler
熟悉WebForm开发的朋友一定都知道,Page类必须实现一个接口,就是IHttpHandler.HttpHandler是一个HTTP请求的真正处理中心,在HttpHandler容器中,ASP.NET ...
- 【深入浅出jQuery】源码浅析2--奇技淫巧
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- 浅析匿名函数、lambda表达式、闭包(closure)区别与作用
浅析匿名函数.lambda表达式.闭包(closure)区别与作用 所有的主流编程语言都对函数式编程有支持,比如c++11.python和java中有lambda表达式.lua和JavaScript中 ...
- word-break|overflow-wrap|word-wrap——CSS英文断句浅析
---恢复内容开始--- word-break|overflow-wrap|word-wrap--CSS英文断句浅析 一 问题引入 今天在再次学习 overflow 属性的时候,查看效果时,看到如下结 ...
随机推荐
- submit 防止重复提交 --禁止提交
<form action="/apply/apply" method="POST" id="indentForm"> <p ...
- Excel教程(4) - 数据库函数
DAVERAGE 用途:返回数据库或数据清单中满足指定条件的列中数值 的平均值. 语法:DAVERAGE(database,field,criteria) 参数:Database 构成 ...
- erlang-string
string:len("abcdef"). 结果为 6 求字符串的长度 string:equal("abc","abc"). 结果为 tru ...
- 飞雪桌面日历软件 V8.6 免费绿色版
软件名称: 飞雪桌面日历软件软件语言: 简体中文授权方式: 免费软件运行环境: Win7 / Vista / Win2003 / WinXP / Win2008软件大小: 4MB图片预览: 软件简介: ...
- Flask -- 入门
安装virtualenv 作用:可以为一个项目单独提供一份Python的安装,安全 pip install virtualenv 使用virtualenv为MyProject项目安装Python,并 ...
- HttpClient的get和post方式提交数据的使用
/** * Http工具类 */ public class HttpUtil { // 创建HttpClient对象 public static HttpClient httpClient = new ...
- iOStextView的代理方法展示
UITextView的代理方法 textViewShouldBeginEditing: and textViewDidBeginEditing: - (BOOL)textViewShouldBegin ...
- ACboy needs your help again!
ACboy needs your help again! Time Limit : 1000/1000ms (Java/Other) Memory Limit : 32768/32768K (Ja ...
- PHP: 异常exception
异常最常见于SDK调用中,函数执行失败时抛出异常,顺带错误码和错误信息. 先来看下PHP的异常处理相关函数: public Exception::__construct() ([ string $me ...
- Hadoop2.6.0 动态增加节点
本文主要从基础准备,添加DataNode和添加NodeManager三个部分详细说明在Hadoop2.6.0环境下,如何动态新增节点到集群中. 基础准备 在基础准备部分,主要是设置hadoop运行的系 ...