如何更深刻使用Moya

Moya 簡介

Moya是一套基於Alamofire的網絡抽象層框架。git

我的認爲Alamofire是基於URLSession上,如何更方便的調用請求,而Moya則是基於Alamofire上,經過抽象 URLs 和 parameter等等,更好的管理API。github

Moya

基本模板

Moya在對於API的封裝是基於enum,經過對於枚舉不一樣端點的不一樣用法,生成請求。json

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 "Half measures are as bad as nothing at all.".data(using: String.Encoding.utf8)!
        case .userProfile(let name):
            return "{\"login\": \"\(name)\", \"id\": 100}".data(using: String.Encoding.utf8)!
        }
    }

    var validationType: ValidationType {
        return .successAndRedirectCodes
    }

    var headers: [String: String]? {
        return nil
    }
}

複製代碼

經過枚舉繼承TargetType,添加細節實現。swift

var provider = MoyaProvider<GitHub>()
provider.request(target) { response in
    if case .failure(let error) = response {
        receivedError = error
    }
}
複製代碼

最後生成根據TargetType生成provider進行請求。api

到此就是Moya的基本實現。由於過於基本,再也不贅述。緩存

Codable

Codable協議是蘋果提供解析數據的協議,在不使用第三方庫,如ObjectMapper, SwiftyJson的狀況下,將服務器返回的JSON數據轉爲model。服務器

下面是一個簡單的Codable示例:網絡

struct Demo: Codable {
    var name: String
    var age: Int
}

func decode() {
    let jsonString =  "{\"name\":\"zhangsan\", \"age\":15}" // 模擬JSON數據
    let decoder = JSONDecoder()
    let data = jsonString.data(using: .utf8)!
    let model = try! decoder.decode(Demo.self, from: data)
    print(model) // Demo(name: "zhangsan", age: 15)
}
複製代碼

MoyaResponse中已經封裝好了對應的處理數據結構

DemoProvider.provider.request(.zen) { (result) in
    switch result {
    case .success(let response):
        if let model = try? response.map(Demo.self) {
            success(model)
        }
    case .failure(let error):
        break
   }
}
複製代碼

若是數據是在JSON的好幾個層級中,也能夠經過設定keypath獲取:app

{
    data: {
        name: "test",
        age: 15
    }
}

try? response.map(Demo.self, atKeyPath: "data")

複製代碼

要注意的是這裏函數還有一個參數叫作failsOnEmptyData,默認設定爲true,若是返回的數據爲空,會斷定會解析失敗。

EndPoint

EndPoint是Moya的半個內部數據結構,由所用的TargetType生成,它最終被用來生成網絡請求。 每一個EndPoint 都存儲了下面的數據:

/// A string representation of the URL for the request.
public let url: String

/// A closure responsible for returning an `EndpointSampleResponse`. (單元測試)
public let sampleResponseClosure: SampleResponseClosure

/// The HTTP method for the request.
public let method: Moya.Method

/// The `Task` for the request.
public let task: Task

/// The HTTP header fields for the request.
public let httpHeaderFields: [String: String]?
複製代碼

在Provider生成時,能夠傳入endpointClosure,自定義TargetType到Endpoint的方式。

默認的實現方式:

final class func defaultEndpointMapping(for target: Target) -> Endpoint {
    return Endpoint(
        url: URL(target: target).absoluteString,
        sampleResponseClosure: { .networkResponse(200, target.sampleData) },
        method: target.method,
        task: target.task,
        httpHeaderFields: target.headers
    )
}
複製代碼

在這裏能夠從新定義Endpoint的生成方式, 好比:

// 將全部生成Endpoint改成get方式請求
let endpointClosure = { (target: MyTarget) -> Endpoint in
    let url = URL(target: target).absoluteString
    return Endpoint(url: url, sampleResponseClosure: {.networkResponse(200, target.sampleData)}, method: .get, task: target.task)
}
複製代碼

或者對已經生成的Endpoint修改:

let endpointClosure = { (target: MyTarget) -> Endpoint in
    let defaultEndpoint = MoyaProvider.defaultEndpointMapping(for: target)
    return defaultEndpoint.adding(newHTTPHeaderFields: ["APP_NAME": "MY_AWESOME_APP"])
}
複製代碼

注意:若是直接對已經初始化的Endpoint修改,只能修改task以及添加header。

Request

在生成Endpoint以後,會從Endpoint再轉爲URLRequst進行使用。

Moya的默認實現:

RequestResultClosure = (Result<URLRequest, MoyaError>) -> Void

final class func defaultRequestMapping(for endpoint: Endpoint, closure: RequestResultClosure) {
    do {
     let urlRequest = try endpoint.urlRequest()
        closure(.success(urlRequest))
    } catch MoyaError.requestMapping(let url) {
        closure(.failure(MoyaError.requestMapping(url)))
    } catch MoyaError.parameterEncoding(let error) {
        closure(.failure(MoyaError.parameterEncoding(error)))
    } catch {
        closure(.failure(MoyaError.underlying(error, nil)))
    }
}


 public func urlRequest() throws -> URLRequest {
        guard let requestURL = Foundation.URL(string: url) else {
            throw MoyaError.requestMapping(url)
        }

        var request = URLRequest(url: requestURL)
        request.httpMethod = method.rawValue
        request.allHTTPHeaderFields = httpHeaderFields

        switch task {
        case .requestPlain, .uploadFile, .uploadMultipart, .downloadDestination:
            return request
        case .requestData(let data):
            request.httpBody = data
            return request
......
複製代碼

由於內部已經實現如何生成Request,大多狀況不須要修改urlRequest,而是從新定義requestClosure, 對已經生成好的request進行修改,下面是直接修改request的緩存策略,以及錯誤處理:

let requestClosure = { (endpoint: Endpoint, done: MoyaProvider.RequestResultClosure) in
    do {
        var request = try endpoint.urlRequest()
        request.cachePolicy = .reloadIgnoringCacheData
        done(.success(request))
    } catch {
        done(.failure(MoyaError.underlying(error)))
    }

}
複製代碼

stubClosure

stubClosure實現:

/// Do not stub.
final class func neverStub(_: Target) -> Moya.StubBehavior {
    return .never
}

/// Return a response immediately.
final class func immediatelyStub(_: Target) -> Moya.StubBehavior {
    return .immediate
}

/// Return a response after a delay.
final class func delayedStub(_ seconds: TimeInterval) -> (Target) -> Moya.StubBehavior {
    return { _ in return .delayed(seconds: seconds) }
}
複製代碼

Moya的默認實現是neverStub,當使用immediatelyStub或者是delayedStub,請求網絡時就不會走真實的數據,而是返回Target中SimpleData的數據,通常用於測試API返回數據的處理。

delayedStub相對於immediatelyStub指定了延遲時長,單位是秒。

callbackQueue

能夠指定網絡請求返回以後的callback線程。默認全部的請求將會被Alamofire放入background線程中, callbac將會在主線程中調用。

Manager

public typealias Manager = Alamofire.SessionManager

final class func defaultAlamofireManager() -> Manager {
    let configuration = URLSessionConfiguration.default
    configuration.httpAdditionalHeaders = Manager.defaultHTTPHeaders

    let manager = Manager(configuration: configuration)
    manager.startRequestsImmediately = false
    return manager
}
複製代碼

Moya中使用的Manager其實就是Alamofire的Manager。

能夠設定Timeout,緩存策略等等

let manager: SessionManager = {
    let configuration = defaultURLSessionConfiguration
    configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
    configuration.timeoutIntervalForRequest = 20
    let trustPolicyManager = ServerTrustPolicyManager(policies:
        [
            "www.baidu.com": ServerTrustPolicy.disableEvaluation
        ]
    )
    let manager = SessionManager(configuration: configuration, serverTrustPolicyManager: trustPolicyManager)
    return manager
}()
複製代碼

Plugins

plugins是遵照了PluginType的插件,一個provider能夠方多個Plugin。

PluginType:

public protocol PluginType {
    /// 在發送request以前,還有機會對request修改
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest

    /// 發送以前調用
    func willSend(_ request: RequestType, target: TargetType)

    /// 接受Response以後,在觸發callback以前
    func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType)

    /// 在調用Callback以前,還能修改result
    func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>
}

public extension PluginType {
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest { return request }
    func willSend(_ request: RequestType, target: TargetType) { }
    func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) { }
    func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError> { return result }
}
複製代碼

在Plugin中能夠作不少事情

  • 記錄網絡請求
  • 處理隱藏或者顯示網絡activity progress
  • 對request進行更多的處理

好比:

struct TestPlugin: PluginType {
    // 對request進行更多的處理
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
        var request = request
        if target is GitHub {
            request.timeoutInterval = 5
        }
        return request
    }
    
    // 記錄網絡請求
    func willSend(_ request: RequestType, target: TargetType) {
        print("start")
        print(request.request?.url ?? "")
    }
    
    // 記錄網絡請求
    func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
        print("end")
        switch result {
        case .success(let response):
            print("end success")
            print(response.request?.url ?? "")
        case .failure(let error):
            print("end failure")
            print(error)
        }
    }
    
    // 對返回的result進行修改
    func process(_ result: Result<Response, MoyaError>, target: TargetType) -> Result<Response, MoyaError> {
        if case let .failure(error) = result {
            return .failure(MoyaError.underlying(error, nil))
        }
        return result
    }
}
複製代碼

Moya也對Logger,activity等提供了默認實現的Plugin,更多細節就不詳細說明了。

trackInflights

源碼看了半天仍是看不明白,但願懂的朋友能告訴我是怎麼用的。

MultiTarget

通常場景下,是一個targetType對應一個Provider

let githubProvider = MoyaProvider<GitHub>(stubClosure: MoyaProvider.immediatelyStub, trackInflights: true)
let demoProvider = MoyaProvider<Demo>(stubClosure: MoyaProvider.immediatelyStub, trackInflights: true)
複製代碼

可是若是像讓這個Provider更通用,能夠寫爲:

let commonProvider = MoyaProvider<MultiTarget>()
複製代碼

調用的時候指定TargetType便可:

commonProvider.request(MultiTarget(GitHub.zen)) { result in
    ...
}
複製代碼

流程

補一張網上找到的流程圖

流程圖
相關文章
相關標籤/搜索