前言
•Moya是一个纯粹的轻量级网络层。依赖于Alamofire(目前Swift最火的网络框架,对比AFNetworking)上封装。Moya是一个高度抽象的网络库,理念是让你不用关心网络请求的底层的实现细节,只用定义你关心的业务。 •使用的好处:
1.网络层和业务层解耦分离
2.单元测试
3.规范&&稳定 • •官方给出几个Moya主要优点: •编译时检查API endpoint权限 •让你使用枚举定义各种不同Target, endpoints •把stubs当做一等公民对待,因此测试超级简单。
Moya的认识
普通的Alamofire网络请求
let parameters: Parameters = [ "foo": [1,2,3], "bar": [ "baz": "qux" ] ] Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: JSONEncoding.default)
Moya的网络请求:
provider = MoyaProvider<GitHub>() provider.request(.userProfile("ashfurrow")) { result in // do something with the result }
本文的学习过程
一.Moya的实现流程图
二.Moya的内部实现过程&使用过程
三.Moya的扩展— >RectiveSwift
一.Moya的实现流程图
Moya的主要实现流程是将用户设置好的Target类型转换成EndPoint再转换成URLRequest给Alamofire做请求,然后将模拟数据(用户设置测试模式)或者Alamofire的返回数据返回到业务层。
二.Moya的内部实现过程&使用过程
1.target
我们从整个流程的入口开始学习,Target是一个遵守TargetType的枚举。这个TargetType协议要求Target设定服务器地址,每一个请求的地,请求方式,样式返回参数(测试使用),任务参数,是否执行Alamofire验证 和请求的头部信息。具体的协议内容如下:
public protocol TargetType { /// The target's base `URL`. var baseURL: URL { get } /// The path to be appended to `baseURL` to form the full `URL`. var path: String { get } /// The HTTP method used in the request. var method: Moya.Method { get } /// Provides stub data for use in testing. var sampleData: Data { get } /// The type of HTTP task to be performed. var task: Task { get } /// Whether or not to perform Alamofire validation. Defaults to `false`. var validate: Bool { get } // The headers to be used in the request. var headers: [String: String]? { get } }
一般我们制造这个Target的方式如下:
enum GitHub{ case zen case userProfile(String) } extension GitHub: TargetType { var baseURL: URL { return URL(string: "https://api.github.com")! } var path: String { switch self { case .zen: return "/zen" case .userProfile(let name): return "/users/\(name)" } } var method: Moya.Method { return .get } var task: Task { return .requestPlain } var sampleData: Data { switch self { case .zen: return "lala".data(using: String.Encoding.utf8)! // return "Half measures are as bad as nothing at all.".data(using: String.Encoding.utf8)! case .userProfile(let name): return "".data(using: String.Encoding.utf8)! // return "{\"login\": \"\(name)\", \"id\": 100}".data(using: String.Encoding.utf8)! } } var validate: Bool { return true } var headers: [String: String]? { return nil } }
一个扩展的枚举,这个枚举遵守了TargetType协议,执行了它里面的每一个函数。给需要的接口设定了对应的信息。当然,在实际的开发过程中,接口分业务模块有很多很多类型的接口,我们可以利用扩展,将不同业务的接口分开管理。也不需要执行重复的协议方法。
2.Provider——–Moya的核心类
Provider的作用
Provider真正做的事情可以用一个流来表示:Target -> Endpoint -> Request 。在这个例子中,它将Target转换成Endpoint, 再将其转换成为NSRURLRequest。最后将这个NSRURLRequest交给Alamofire去进行网络请求。
Provider初始化的时候做了2个事情:
1.你指定了Target到Endpoint的映射。
2.你指定了Endpoint到URLRequest的映射。
首先,我们看看Provider的初始化函数
public init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping, requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping, stubClosure: @escaping StubClosure = MoyaProvider.neverStub, callbackQueue: DispatchQueue? = nil, manager: Manager = MoyaProvider<Target>.defaultAlamofireManager(), plugins: [PluginType] = [], trackInflights: Bool = false) { self.endpointClosure = endpointClosure self.requestClosure = requestClosure self.stubClosure = stubClosure self.manager = manager self.plugins = plugins self.trackInflights = trackInflights self.callbackQueue = callbackQueue }
Provider获得了好3个闭包。
1.我们发现的是:endpointClosure、requestClosure、stubClosure。这3个Closure是让我们定制请求或进行测试时用的。
2.然后是一个Manager,Manager是真正用来网络请求的类,Moya自己并不提供Manager类,Moya只是对其他网络请求类进行了简单的桥接。这么做是为了让调用方可以轻易地定制、更换网络请求的库。比如你不想用Alamofire,可以十分简单的换成其他库
3.最后是一个类型为PluginType的数组。Moya提供了一个插件机制,使我们可以建立自己的插件类来做一些额外的事情。比如写Log,显示“菊花”等。抽离出Plugin层的目的,就是让Provider职责单一,满足开闭原则。把和自己网络无关的行为抽离。避免各种业务揉在一起不利于扩展。
以下是初始化参数的认识:
1. EndPointClosure的作用,默认值 和 定制
作用:
EndpointClosure这个闭包,输入是一个Target,返回Endpoint。这就是我们前面说Provider指定Target 到Endpoint的映射
Endpoint 是Moya最终进行网络请求前的一种数据结构,它保存了这些数据:
•URL •HTTP请求方式 (GET, POST, etc). •本次请求的参数 •参数的编码方式 (URL, JSON, custom, etc). •stub数据的 response(测试用的)
默认值:
public final class func defaultEndpointMapping(for target: Target) -> Endpoint<Target> { return Endpoint( url: URL(target: target).absoluteString, sampleResponseClosure: { .networkResponse(200, target.sampleData) }, method: target.method, task: target.task, httpHeaderFields: target.headers ) }
类函数, 这个函数输入一个Target类型(范型,这个类型由外部决定,在Provider实例化的时候设定,官方的例子里Target类型就是Github)的target, 输出一个Endpoint, 这个函数将是Provider的默认参数。将赋给一个闭包参数。
定制:
默认闭包的代码只是单纯地创建并返回一个Endpoint实例。然而在很多时候,我们需要自定义这个闭包来做更多额外的事情。例如给客户端返回一个非200的状态码。为了实现这个功能,就需要在这个闭包里处理相关的逻辑!或者说这个闭包就是让我们根据业务需求定制网络请求的
以下例子是添加头参数
let endpointClosure = { (target: GitHub) -> Endpoint<GitHub> in let defaultEndpoint = MoyaProvider.defaultEndpointMapping(for: target) return defaultEndpoint.adding(newHTTPHeaderFields: ["APP_NAME": "MY_AWESOME_APP"]) } let provider = MoyaProvider<GitHub>(endpointClosure: endpointClosure)
以下是添加认证Token的例子
let endpointClosure = { (target: GitHub) -> Endpoint<GitHub> in let defaultEndpoint = MoyaProvider.defaultEndpointMapping(for: target) // Sign all non-authenticating requests switch target { case .zen: return defaultEndpoint default: return defaultEndpoint.adding(newHTTPHeaderFields: ["AUTHENTICATION_TOKEN": GlobalAppStorage.authToken]) } } let provider = MoyaProvider<GitHub>(endpointClosure: endpointClosure)
2. requestClosure的作用,默认值 和 定制
作用:
Endpoint到Request的映射
默认值:
public final class func defaultRequestMapping(for endpoint: Endpoint<Target>, closure: RequestResultClosure) { if let urlRequest = endpoint.urlRequest { closure(.success(urlRequest)) } else { closure(.failure(MoyaError.requestMapping(endpoint.url))) } }
简单地调用endpoint.urlRequest取得一个NSURLRequest实例。然后调用了closure回调
定制:
用于修改针对URLRequest的属性修改 或者 提供信息给请求(必须请求生成)。例如Cookies设置。
let reuestClosure = { (endpoint: Endpoint<GitHub>, done: MoyaProvider.RequestResultClosure) in var request: URLRequest = endpoint.urlRequest! request.httpShouldHandleCookies = false done(.success(request)) } let provider = MoyaProvider<GitHub>(requestClosure: reuestClosure)
3. requestClosure的作用
StubClosure这个闭包比较简单,返回一个StubBehavior的枚举值。它就是让你告诉Moya你是否使用Stub返回数据或者怎样使用Stub返回数据
public enum StubBehavior { /// Do not stub. case never /// Return a response immediately. case immediate /// Return a response after a delay. case delayed(seconds: TimeInterval) }
Never表明不使用Stub来返回模拟的网络数据,Immediate表示马上返回Stub的数据,Delayed是在几秒后返回。Moya默认是不使用Stub来测试。
在Target那一节我们定义了一个GitHub, 我们实现了接口sampleData,这个属性是返回Stub数据的。
Moya先调用我们在构造函数中传入的stubClosure闭包,如果stubBehavior是Never就真正的发起网络请求,否
者就调用self.stubRequest
如果Immediate,就马上调用stub返回,是Delayed的话就Dispatch after延迟调用。
4.Manager的作用, 默认值 和 定制
Moya旨在隐藏Alamofire,做了桥接。也便于脱离Alamofire。用了2种方式:
1.别名方式
public typealias Manager = Alamofire.SessionManager internal typealias Request = Alamofire.Request internal typealias DownloadRequest = Alamofire.DownloadRequest internal typealias UploadRequest = Alamofire.UploadRequest internal typealias DataRequest = Alamofire.DataRequest internal typealias URLRequestConvertible = Alamofire.URLRequestConvertible /// Represents an HTTP method. public typealias Method = Alamofire.HTTPMethod /// Choice of parameter encoding. public typealias ParameterEncoding = Alamofire.ParameterEncoding public typealias JSONEncoding = Alamofire.JSONEncoding public typealias URLEncoding = Alamofire.URLEncoding public typealias PropertyListEncoding = Alamofire.PropertyListEncoding /// Multipart form public typealias RequestMultipartFormData = Alamofire.MultipartFormData /// Multipart form data encoding result. public typealias MultipartFormDataEncodingResult = Manager.MultipartFormDataEncodingResult public typealias DownloadDestination = Alamofire.DownloadRequest.DownloadFileDestination
2.抽象了一个RequestType协议,利用这个协议将Alamofire隐藏了起来,让Provider类依赖于这个协议 ,然后让Moya.Manager == Alamofire.Manager,并且让Alamofire.Manager也实现RequestType协议
public protocol RequestType { // Note: // // We use this protocol instead of the Alamofire request to avoid leaking that abstraction. // A plugin should not know about Alamofire at all. /// Retrieve an `NSURLRequest` representation. var request: URLRequest? { get } /// Authenticates the request with a username and password. func authenticate(user: String, password: String, persistence: URLCredential.Persistence) -> Self /// Authenticates the request with an `NSURLCredential` instance. func authenticate(usingCredential credential: URLCredential) -> Self }
完成了Alamofire的封装、桥接。正因为桥接封装了Alamofire,因此Moya的request,最终一定会调用Alamofire的request
func sendRequest(_ target: Target, request: URLRequest, callbackQueue: DispatchQueue?, progress: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> CancellableToken { let initialRequest = manager.request(request as URLRequestConvertible) let alamoRequest = target.validate ? initialRequest.validate() : initialRequest return sendAlamofireRequest(alamoRequest, target: target, callbackQueue: callbackQueue, progress: progress, completion: completion) }
默认值:
public final class func defaultAlamofireManager() -> Manager { let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = Manager.defaultHTTPHeaders let manager = Manager(configuration: configuration) manager.startRequestsImmediately = false return manager }
定制:
如果您喜欢自定义自己的 manager, 比如, 添加SSL pinning, 创建一个并且添加到manager, 所有请求将通过自定义配置的manager进行路由.
let policies: [String: ServerTrustPolicy] = [ "example.com": .PinPublicKeys( publicKeys: ServerTrustPolicy.publicKeysInBundle(), validateCertificateChain: true, validateHost: true )] let manager = Manager(configuration: URLSessionConfiguration.default, serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) ) let provider = MoyaProvider<MyTarget>(manager: manager)
5. 插件
最后,你可能会想提供一系列的插件给Provider,那些发送请求前和接受相应后的回调!!
自定义插件必须要遵从PluginType协议,协议的回调暴露了请求的过程,具体函数如下:
public protocol PluginType { /// Called to modify a request before sending func prepare(_ request: URLRequest, target: TargetType) -> URLRequest /// Called immediately before a request is sent over the network (or stubbed). func willSend(_ request: RequestType, target: TargetType) /// Called after a response has been received, but before the MoyaProvider has invoked its completion handler. func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) /// Called to modify a result before completion func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError> }
例子:
Moya已经有一些插件供使用,一个是网络activity(NetworkActivityPlugin),一个是网络活动日记(NetworkLoggerPlugin),和HTTP认证。 •例如你可以简单地加入[NetworkLoggerPlugin()]在你Endpoint的插件参数旁边。 注意:插件也是可设置的,例如已经加入了NetworkActivityPlugin 需要一个 networkActivityClosure 参数。这个设置插件实现如下:
// Network activity change notification type. public enum NetworkActivityChangeType { case began, ended } /// Notify a request's network activity changes (request begins or ends). public final class NetworkActivityPlugin: PluginType { public typealias NetworkActivityClosure = (_ change: NetworkActivityChangeType) -> Void let networkActivityClosure: NetworkActivityClosure public init(networkActivityClosure: @escaping NetworkActivityClosure) { self.networkActivityClosure = networkActivityClosure } // MARK: Plugin /// Called by the provider as soon as the request is about to start public func willSend(_ request: RequestType, target: TargetType) { networkActivityClosure(.began) } /// Called by the provider as soon as a response arrives, even if the request is canceled. public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) { networkActivityClosure(.ended) } }
使用的时候:
let network = { (_ change: NetworkActivityChangeType) -> Void in switch change { case .began: print("我是自定义的网络监控————开始啦") default: print("我是自定义的网络监控---结束啦") } } let activity:NetworkActivityPlugin = NetworkActivityPlugin(networkActivityClosure:network) let provider = MoyaProvider<GitHub>(endpointClosure: endpointClosure,plugins:[activity])
Moya官方还提供了请求的日记打印插件 NetworkLoggerPlugin.
可以直接使用。
let provider = MoyaProvider<GitHub>(plugins: [NetworkLoggerPlugin(verbose: true)])
例外还有插件:
认证:
Sources/Moya/Plugins/CredentialsPlugin.swift
当你想自定义插件的时候,可查阅:
和
ReactiveSwift响应式编程
•Moya提供了Provider的选择性ReactiveSwift执行,这个能做一些好玩的东西。不再使用调用request()方法和请求完成后提供回调,使用了SignalProducer s。
•使用reactive扩展,我们不需要任何的装载,直接使用MoyaProvider的实例对象。
注意:Pod文件需要
pod 'Moya/ReactiveSwift'
例子:
provider.reactive.request(.zen).start { event in switch event { case let .value(response): break // do something with the data case let .failed(error): break // handle the error default: break } } //或者 provider.reactive.requestWithProgress(.zen).start { event in switch event { case .value(let progressResponse): if let response = progressResponse.response { // do something with response } else { print("Progress: \(progressResponse.progress)") } case .failed(let error): break print(error) // handle the error default: break } }
有一点需要记得:
请求在没有信号订阅的情况下不会进行请求。如果定远在请求完成前被丢弃,请求会被取消。
如果请求正常完成,两个事情发生:
1.信号发送值,一个Moya.Response对象
2.信号完成
如果请求生成一个错误(特别氏URLSession错误),那么它会发送一个error。错误代码是请求失败代码,还有data
请求结果的处理
•Moya.response类包含有statusCode,data和一个(n 选择性的)HTTPURLResponse。
•Moya提供了SinalProducer的扩张去相当容易地处理Moya.response,分别有:
•filter(statusCodes:) 获取某个状态码区间的相应,如果超出,返回error.
•filter(statusCode:) 寻找指定的状态嘛,其它的返回error.
•filterSuccessfulStatusCodes() 过滤200-range间的状态码.
•filterSuccessfulStatusAndRedirectCodes() 过滤200-300的状态码.
•mapImage() 尝试映射响应到UIImage实例,失败返回error.
•mapJSON() 尝试映射响应到JSON对象,失败返回error.
•mapString() 尝试映射响应到String,失败返回error.
•mapString(atKeyPath:) 尝试映射一个响应data的对应key的值成一个String,失败返回error.
请求结果处理—error说明
•在Error里,error的domain是MoyaErrorDomain。 码是MoyaErrorCode 的其中一个rawValue。
•只要有可能,就会提供潜在的错误和原始响应数据在NSError的userinfo字典的对应data的值里。
总结
学习了swift很受欢迎网络封装层(Moya),它给网络层提供了规范,稳定,易扩展,易管理等性能!期间使用了闭包回调,类映射(脱耦),枚举应用,范型使用等让我更深地认识Swift以及感觉到它的强大。
Moya主要供功能包括如下:
1.分离App业务层和网络层,统一管理(枚举)接口文件
2.提供闭包自定义设定Provider配置&&插件扩展,覆盖了网络常见的功能(如:SSL,请求头管理,Token认证,Cookie设置,验证码过滤,网络过层监控,运行日记,映射功能,单元测试等)