Moya-Swift网络层

前言

•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

当你想自定义插件的时候,可查阅:

docs/Examples/CustomPlugin.md

docs/Examples/AuthPlugin.md.

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设置,验证码过滤,网络过层监控,运行日记,映射功能,单元测试等)

发表评论

邮箱地址不会被公开。 必填项已用*标注