Moya + Alamofire + HandyJson + RxSwift 搭建一個新項目的網絡請求

一、前言

提及來汗顏。git

最近項目纔開始使用 Swift 語言,正如我一個朋友嘲笑的:咱們都快用爛的東西大家纔開始用 ,我當時竟無言以對。github

那既然用了 Swift,就要想辦法用舒服,用明白。從 OC 工程轉換到 Swift 工程,OC 的一些庫,好比:網絡請求庫(AFNetworking),Json解析(YYModel), 響應式編程(RAC),還有網絡請求的封裝庫(本身封裝的或者第三方的) 就要按需更換了。編程

二、第三庫的選擇

一、網絡請求庫

毫無疑問是 Alamofire 了,就和 OC 項目選擇 AFNetworking 同樣。json

二、Json 解析

Swift 也有很多,好比 SwiftyJSONHandyJSON 等。swift

SwiftyJSON 很是強大,能幫助開發者將 Json 轉成字典,按照 key 值取出時也幫助開發者進行路徑判空,可是,我我的感受用起來有點奇怪。api

後來選擇了阿里的 HandyJSONHandyJSON 也支持結構體,支持將 Json 轉成對象,支持模型數組,由於 Swift 上對泛型的支持,因此對比 OC 上的 YYModel 用起來更舒服些。數組

三、響應式編程

Swift 是靜態語言,採用鏈式函數編程,Swift 中使用響應式編程,會讓 Swift 更加簡單和輕巧。服務器

目前能夠選擇有不少,好比 ReactiveCocoa(Swift)RxSwiftSwift Combine(蘋果本身的),各有優勢缺點,各位客官能夠自由比對選擇,若是第一次接觸的話,就本身隨意選一個(畢竟使用過了才能對比)。markdown

  • RxSwift 維護人員較多,這意味着你能輕易找到問題的解決方案,而且 RxSwiftReactiveX 的一個而已,它還有 RxJavaRxPython 等等。學會了一個,說不定其餘都是同樣哦。網絡

  • ReactiveCocoa(Swift),這個是從 OC 上翻譯過來的,有一些歷史的 OC 包袱,可是原來熟悉 RAC 的會更容易上手。

  • Swift Combine 是蘋果本身的,本身的親兒子,將來更新的概率會更大,而且不會出現第三庫不在維護更新的。

四、網絡庫封裝

若是大家公司 OC 項目上,有在網絡庫上再次封裝的好用、強大的庫,那麼這個你就不用看了,你確定只能混編。

對於以前本身項目只有簡單再封裝 AFNetworking 或者是新項目的,推薦使用 Moya

Moya只是對 Alamofire 的再次封裝,並非網絡請求庫,因此使用 Moya就須要使用 Alamofire

既然是網絡庫的再次封裝,那麼就能夠將 Alamofire 替換成其餘的,只須要重寫 Moya+Alamofire.swift 就能夠了。我我的感受通常不必。

三、使用方法

Moya 是對 Alamofire 的再封裝,若是隻是使用的話,關心 Moya 的使用方法便可。

Moya 分別提供了Moya英文文檔Moya中文文檔。(英文文檔更全面)

一、熟悉 Moya

image.png

下載官方的 Demo 後,先熟悉一下 Moya 的用法。

文檔已經很詳細,這裏簡單說明一下

/// 建立一個文件 MyService.swift

/// 聲明一個枚舉
enum MyService {
    /// 分類放置你的請求調用函數
    case createUser(firstName: String, lastName: String) } /// 擴展你的枚舉,遵照 TargetType 協議 extension MyService: TargetType {
    var baseURL:  {
        /// 放入 host
        return baseURL;
    }
    var path: String {
        case createUser(let firstName, let lastName) /// 返回具體請求路徑 return "/user/create/user" } var method: Moya.Method {
        switch self {
        case .createUser:
            /// 返回 .get 或者 .post
            return .post;
        }
    }
    
    var task: Task {
        switch self {
        case .createUser(let firstName, let lastName): 
            /// 具體請求參數
            return .requestParameters(parameters: ["first_name": firstName, "last_name": lastName], encoding: JSONEncoding.default)
        }
    }
    
    var sampleData: Data {
        /// 若是服務器給了測試示例,能夠放到這裏
        case .createUser(let firstName, let lastName): 
           return "{\"id\": 100, \"first_name\": \"\(firstName)\", \"last_name\": \"\(lastName)\"}".utf8Encoded 
    }
    
    var headers: [String: String]? {
        /// 請求頭設置
        return ["Content-type": "application/json"]
    }
}

複製代碼

而後你就能夠在你的 ViewController 調用了:

let provider = MoyaProvider<MyService>()
provider.request(.createUser(firstName: "James", lastName: "Potter")) { result in
    // do something with the result (read on for more details)
}

// The full request will result to the following:
// POST https://api.myservice.com/users
// Request body:
// {
// "first_name": "James",
// "last_name": "Potter"
// }
複製代碼

二、瞭解 Moya

上面只是初步使用了一下 Moya,可是具體業務遠比 Demo 複雜的多,Moya 也給咱們提供至關充足的施展空間。

第一步仍是建立一個文件,聲明一個枚舉,實現 TargetType 協議。可是建立 MoyaProvider 對象就不一樣了。

上方代碼只是使用了 let provider = MoyaProvider<MyService>() 建立,其實 MoyaProvider 中還有其餘參數的。具體來看一下:

/// Initializes a 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
    }

    /// Returns an `Endpoint` based on the token, method, and parameters by invoking the `endpointClosure`.
    open func endpoint(_ token: Target) -> Endpoint {
        return endpointClosure(token)
    }
複製代碼

這裏看到 MoyaProvider 對象 init 的時候還額外提供了 7 個參數,只是若是你使用了默認的 init,其餘會被自動賦上默認值。

一、endpointClosure

默認源碼以下:

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
        )
}
複製代碼

這裏是將建立的遵照協議的枚舉 MyService 轉化成 Endpoint,每每咱們只是使用它的默認方法。 查閱 Endpoint ,發現還提供了兩個方法:

  • open func adding(newHTTPHeaderFields: [String: String]) -> Endpoint :用於更改請求頭。

  • open func replacing(task: Task) -> Endpoint : 將原有 MyService 枚舉中實現的 task 進行替換。

可是有時候也有業務測試的需求,如:網絡錯誤,超時等。就能夠在這裏實現。

Moya官方解釋:因爲它是一個閉包, 它將在每次調用API時被執行, 因此你能夠作任何你想要的操做。

Moya 給了一個例子,只須要將對象 failureEndpointClosure 傳入 MoyaProvider 的參數endpointClosure 便可。

let failureEndpointClosure = { (target: MyService) -> Endpoint in
    let sampleResponseClosure = { () -> (EndpointSampleResponse) in
        if shouldTimeout {
            return .networkError(NSError())
        } else {
            return .networkResponse(200, target.sampleData)
        }
    }
    return Endpoint(url: URL(target: target).absoluteString, 
        sampleResponseClosure: sampleResponseClosure, 
        method: target.method, 
        task: target.task)
}
複製代碼

這裏能夠將 MyService 轉化成 Endpoint 對象的時候能夠任意改變參數,知足各類測試需求。

二、requestClosure

根據 Endpoint 生成 URLRequest

默認源碼以下:

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)))
    }
}
複製代碼

代碼中看到,經過 let urlRequest = try endpoint.urlRequest() 方式由 Endpoint 生成一個 URLRequest對象,就意味着能夠修改 URLRequest 中的參數,好比須要給 URLRequest 設置 timeoutInterval 等。

示例以下:

let requestClosure = { (endpoint: Endpoint, done: MoyaProvider.RequestResultClosure) in
    do {
        var request: URLRequest = try endpoint.urlRequest()
        request.httpShouldHandleCookies = false
        request.timeoutInterval = 15
        done(.success(request))
    } catch {
        done(.failure(MoyaError.underlying(error, nil)))
    }
}
複製代碼
三、stubClosure

這個參數提供了3個枚舉:

  • .never (默認的):直接請求服務器;

  • .immediate:走協議中 sampleData 示例數據;

  • .delayed(seconds) 能夠把 stub 請求延遲指定時間,例如, .delayed(0.2) 能夠把每一個 stub 請求延遲 0.2s 。 這個在單元測試中來模擬網絡請求是很是有用的。

官方示例:

let stubClosure =  { target: MyService -> Moya.StubBehavior in
    switch target {
        /* Return something different based on the target. */
    }
}
複製代碼
四、callbackQueue

回調線程。

五、manager

這裏直接使用官方解釋了,大多工程這裏都用默認的。

接下來就是 session 參數,默認會得到一個經過基本配置進行初始化的自定義的 Alamofire.Session 實例對象

final class func defaultAlamofireSession() -> Session {
    let configuration = URLSessionConfiguration.default
    configuration.headers = .default
    
    return Session(configuration: configuration, startRequestsImmediately: false)
}
複製代碼

這兒只有一個須要注意的事情:因爲在 AF 中建立一個 Alamofire.Request 對象時默認會當即觸發請求,即便爲單元測試進行 "stubbing" 請求也同樣。 所以在Moya中, startRequestsImmediately 屬性被默認設置成了 false

若是你須要自定義本身的 session , 好比說建立一個 SSL pinning 而且添加到 session 中,全部請求將經過自定義配置的 session 進行路由。

let serverTrustManager = ServerTrustManager(evaluators: ["example.com": PinnedCertificatesTrustEvaluator()])

let session = Session(
    configuration: configuration, 
    startRequestsImmediately: false, 
    serverTrustManager: serverTrustManager
)

let provider = MoyaProvider<MyTarget>(session: session)
複製代碼
六、plugins

plugins 是一個攔截器數組,能夠傳入多個遵照 PluginType 協議的對象。查閱 PluginType 協議:

/// - inject additional information into a request
public protocol PluginType {
    /// Called to modify a request before sending.
    /// requestClosure 生成 URLRequest 生成以後回調此方法
    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. /// 收到數據,Moya 尚未進行處理是回調此方法 func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) /// Called to modify a result before completion. /// 在網絡 callBack 閉包回調前回調此方法 func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError> } 複製代碼

這裏能乾的事情太多。

  • 好比:func prepare(_ request: URLRequest, target: TargetType) -> URLRequest 方法回調後,能夠將公共參數(版本號,token,userid)進行拼接,或者對數據進行 RSA 加密加簽。

舉個 🌰 :

/// Called to modify a request before sending.
public func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
    /// 這裏作公共參數
    
    let target = target as! MyService
    var parameters : [String: Any]?
    if let requstData = request.httpBody {
        do {
            let json = try JSONSerialization.jsonObject(with: requstData, options: .mutableContainers)
            parameters = json as? [String: Any]
        } catch  {
            /// 失敗處理 ...
        }
    } else {
        parameters = [String: Any]()
    }
    
    /// 拼接公共參數
    parameters = paramsForPublicParmeters(parameters: parameters)
    
    /// 加密加簽
    parameters = RSA.sign(withParamDic: parameters)
    
    do {
        /// 替換 httpBody
        if let parameters = parameters {
            return try request.encoded(parameters: parameters, parameterEncoding: JSONEncoding.default)
        }
    } catch  {
        /// 失敗處理 ...
    }
    
    return request
}
複製代碼
  • 好比:func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError> 方法回調後,能夠對數據進行驗籤解密。

舉個 🌰 :

/// Called to modify a result before completion.
public func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError> {
    
    /// 驗籤
    if case .success(let response) = result {
        do {
            let responseString = try response.mapJSON()
            
            /// Json 轉成 字典
            let dic =  JsonToDic(responseString)
            
            /// 驗籤
            if let _ = SignUntil.verifySign(withParamDic: dic) {
                
                /// 數據解密
                dic = RSA.decodeRSA(withParamDic: dic)
                
                /// 從新生成 Moya.response
                /// ...
                
                /// 返回 Moya.response
                return .success(response)
            } else {
                let error = NSError(domain: "驗籤失敗", code: 1, userInfo: nil)
                return .failure(MoyaError.underlying(error, nil))
            }
        } catch {
            let error = NSError(domain: "攔截器 response 轉 json 失敗", code: 1, userInfo: nil)
            return .failure(MoyaError.underlying(error, nil))
        }
    } else {
        /// 本來就失敗了就丟回了
        return result
    }
}
複製代碼
  • 你還能夠在 willSenddidReceive 作日誌打印:

舉個 🌰 :

/// 準備發送的時候攔截打印日誌
public func willSend(_ request: RequestType, target: TargetType) {
    /// 請求日誌打印
    NetWorkingLoggerOutPut.outPutLoggerRequest(request.request, andRequestURL: request.request?.url?.absoluteString)
}

/// 將要接受的時候攔截打印日誌
public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
    /// 返回日誌打印
    switch result {
    case .success(let response):
        NetWorkingLoggerOutPut.outPutLoggerReponseString(response.response, andRequest: response.request, andResponseObj:tryResponseToJSON(response: response) )
    case .failure(let error):
        NetWorkingLoggerOutPut.outPutLoggerReponseString(error.response?.response, andRequest: error.response?.request, andResponseObj: tryResponseToJSON(response: error.response))
    }
}
複製代碼

固然,這只是一些代碼片斷,可是重要代碼已經貼出來了,你能夠以此爲靈感繼續擴展。

七、trackInflights

一個請求在 init 的時候將 trackInflights 設置爲 true,那麼在 Moya 中就會存儲這個請求的 endpoint。在返回數據的時候,若是須要跟蹤了重複請求,那麼就將一次實際發送請求返回的數據,屢次返回。

三、使用 Moya

3.1 和 3.2 基本上對 Moya 的使用詳細說明了,這裏就說調用方式吧。

一、普通調用方式

let provider = MoyaProvider(endpointClosure: endpointClosure,
                        requestClosure: requestClosure,
                        stubClosure: stubClosure,
                        manager: manager,
                        plugins: plugins)
                        
provider.request(.createUser("三","張")) { result in
    do {
        let response = try result.get()
        let value = try response.mapNSArray()
        self.repos = value
    } catch {
        let printableError = error as CustomStringConvertible
        self.showAlert("GitHub Fetch", message: printableError.description)
    }
}
複製代碼

二、RxSwift 調用方式

若是使用 RxSwift 須要導入庫 RxMoya,根據 Moya 官方主頁導入便可。

provider.rx.request(.createUser("三","張"))
    .asObservable()
    .mapJSON()
    .mapHandyModel(type: UserModel.self)
    .asSingle()
    .subscribe { (userModel) in
        
    } onFailure: { (error) in
        
    } onDisposed: {
        
    }
    .disposable(by:disposable)
複製代碼

三、Moya 的二次封裝

看完上面的內容,應該對 Moya 有必定的瞭解了,實際開發中,咱們須要涉及的東西至關的多。好比,不一樣的接口可能須要不一樣的網絡超時時間、還能可能須要配置接口需不須要對用戶信息的驗證,是否走本地測試數據,等等。

還有一些,好比 baseURL ,網絡請求頭 headersHTTPMethod 大多都是同樣的,若是每次都從新設置,那有一天改了 baseURL 的地址,headers 都須要增長一個參數,那時候殺人的心都有了。

一、擴展 TargetType 協議

既然 Moya 已經提供了 TargetType 咱們何不擴展一下呢?

public protocol BaseHttpAPIManager: TargetType {
    
    ///是否驗證用戶身份
    var validUser : Bool { get }
    
    ///超時時間
    var timeoutInterval : Double { get }
    
    /// 是否走測試數據 默認 .never
    var stubBehavior: Moya.StubBehavior { get }
    
    /// 等等 ... 
    
}
複製代碼

協議繼承完成以後,這裏就能夠對咱們基本不變化的參數進行賦值。

extension BaseHttpAPIManager {
  
    public var baseURL: URL {
        return URL(string: WebService.shared.BaseURL)!
    }
    
    public var method: Moya.Method {
        return .post
    }
    
    public var sampleData: Data {
        return "response: test data".data(using: String.Encoding.utf8)!
    }
    
    public var task: Task {
        return .requestPlain
    }
    
    ///是否驗證成功碼
    public var validationType: Moya.ValidationType {
        return .successCodes
    }
    
    ///請求頭
    public var headers: [String : String]? {
        return WebService.shared.HttpHeaders
    }
    
    
    ///如下爲自定義擴展
    
    public var validUser : Bool {
        return WebService.shared.ValidUser
    }
    
    public var timeoutInterval : Double {
        return WebService.shared.TimeoutInterval
    }
    
    /// 是否走測試數據 默認 .never
    public var stubBehavior: StubBehavior {
        return .never
    }
    
     //...
}
複製代碼

由於 TargetType 協議是貫穿 Moya 整個核心的,因此你基本能夠在任意地方使用它。以後只須要實現遵照 BaseHttpAPIManager 協議就能夠了。

二、將 MoyaProvider 的建立封裝

這裏我就不寫代碼了,我推薦一個 GitHub 上的 Demo 看一下,本菜雞也是從這裏借鑑的。

四、使用 HandyJson

由於 HandyJson 能夠支持結構體。Swift 中若是不須要繼承的類,建議使用結構體,佔用內存更小。

一、聲明

聲明一個 struct 或者 class,必須支持 HandyJSON 協議。

struct UserModel : HandyJSON {
    var name    : String?
    var age     : Int?
    var address : String?
    var hobby   : [HobbyModel]? /// 支持模型數組,可是須要將數組中類型寫清楚
}
複製代碼

二、使用

/// 普通模型轉換
let parsedElement = UserModel.deserialize(from: AnyObject)

/// 數組模型轉換
let parsedArray = [UserModel].deserialize(from: AnyObject)

複製代碼

三、聯合 RxSwfit 使用

擴展 Observable 就能夠了。

public extension Observable where Element : Any {
    
    /// 普通 Json 轉 Model
    func mapHandyModel <T : HandyJSON> (type : T.Type) -> Observable<T?> {
        return self.map { (element) -> T? in
        
            /// 這裏的data 是 String 或者 dic
            let data = element
            
            let parsedElement : T?
            if let string = data as? String {
                parsedElement = T.deserialize(from: string)
            } else if let dictionary = data as? Dictionary<String , Any> {
                parsedElement = T.deserialize(from: dictionary)
            } else if let dictionary = data as? [String : Any] {
                parsedElement = T.deserialize(from: dictionary)
            } else {
                parsedElement = nil
            }
            return parsedElement
        }
    }
    
    // 將 Json 轉成 模型數組
    func mapHandyModelArray<T: HandyJSON>(type: T.Type) -> Observable<[T?]?> {
        return self.map { (element) -> [T?]? in
        
            /// 這裏的data 是 String 或者 dic
            let data = element
            
            let parsedArray : [T?]?
            if let string = data as? String {
                parsedArray = [T].deserialize(from: string)
            } else if let array = data as? [Any] {
                parsedArray = [T].deserialize(from: array)
            } else {
                parsedArray = nil
            }
            return parsedArray
        }
    }
}
複製代碼

聯合方式上方 3.3.2 Moya RxSwift 調用方式 已經給出了。

json.rx.mapHandyModel(type: UserModel.self)
    .asSingle()
    .subscribe { (userModel) in
        
    } onFailure: { (error) in
        
    } onDisposed: {
        
    }
    .disposable(by:disposable)
複製代碼

五、RxSwift

關於 RxSwift 的使用方式看 Cooci 的博客 RxSwift 用法

六、總結

有了這些,你就能夠快速搭建新項目的網絡請求了,若是感受幫助了你些許,能給個贊最好了,感謝各位。

相關文章
相關標籤/搜索