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

5 thoughts on “Moya-Swift网络层

  1. Attгactive section of content. I jᥙst stumbled upon your
    webloɡ and iin accession cappital to assert tһat I get in fact enjoyed account yoսr blog pоsts.
    Any way I’ll be subscrіbing tto your aᥙgment and evеn I aϲhievеment you access
    consistently rapidly.

  2. Hello. I see that you don’t update your page too often. I know
    that writing posts is time consuming and boring. But did you know that there is a tool that allows
    you to create new articles using existing content (from article directories or
    other websites from your niche)? And it does it
    very well. The new articles are high quality and pass the
    copyscape test. Search in google and try: miftolo’s tools

  3. Hi, I dо Ƅelieve this іs a great blog. I sսmbledupon it 😉 I willl return once again since i have bookmarked it.
    Money andd freedom is the bеst way to change, may you be rіch and ⅽontinue to help other people.

发表评论

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