應該是最詳細的-swift Moya+handyJSON網路框架的搭建及封裝

liaoWorkin發表於2018-04-09

踩坑踩了4天總算把基於Moya的網路框架搭建完畢

看網上關於Moya的教程不太多,大多都是一樣的,還有一些年久失修。這裡專門講講關於moya的搭建及容易遇到的一些坑。

重要的東西放到最前面

1.最好的教材是官方文件和Demo,Moya有中文文件

2.嘗試一些不一樣的東西會讓開發更有趣。

3.我把Demo地址放最後了。

為什麼選擇moya:

一開始網路框架的選型有Alamofire和Moya。

Alamofire可以說是Swift版本的AFN,啃AFN的老啃了幾年了,AFN的確博大精深,有很多值得開發者去學校的地方。但開發這麼多年,AFN實在是啃不動了。試著封裝了一下Alamofire。感覺和AFN封裝大同小異。

和技術群裡的一些大佬討論了一下,大多數也是推薦Moya,至於聊天記錄裡面提及的 包含?地址的問題 我們在稍後的內容裡去解決。後來咬咬牙就決定使用Moya用新專案的網路框架。

image

About Moya

已經有大神把Moya的基本使用和各個模組的介紹說的很清楚了,這裡就不贅述了,建議把框架的基本使用瞭解一番【iOS開發】Moya入坑記-用法解讀篇

上文作為入門是一篇不錯的文章,但作為實際開發過程中,健壯全方位考慮的網路框架來說的來說還有很多用法並沒有提及。 而且網上很多文章都是老版本,看的時候會感覺有些懵。。。所以我就寫了本文?

Let's Begin

####封裝的目錄結構

安裝好Moya後我們建立好三個空的Swift檔案

image

我們大致可將網路框架拆分成

API.swift---將來我們的介面列表和不同的介面的一些配置在裡面完成,最長打交道的地方。

NetworkManager.swift---基本框架配置及封裝寫到這裡

MoyaConfig.swift---這個其實可有可無的,習慣上把baseURL和一些公用字串放進來

OK我們正式開始coding!

API.swift中先建立一個API的列舉,列舉值是介面名, 並建立遵守TargetType協議的extention。

這裡我寫三個測試的Api。第一個是無參,第二個是普通寫法(我看官方文件好像是這種 多引數 都寫進去的,實際開發過程中感覺有些麻煩),第三個是直接把所有引數包裝成字典傳進來的文藝寫法。。

image

直接點選錯誤程式碼補全即可自動補全所有的協議

import Foundation
import Moya

enum API {
    case testApi//無引數的介面
    //有引數的介面
    case testAPi(para1:String,para2:String)//普遍的寫法
    case testApiDict(Dict:[String:Any])//把引數包裝成字典傳入--推薦使用
}

extension API:TargetType{
    
    //baseURL 也可以用列舉來區分不同的baseURL,不過一般也只有一個BaseURL
    var baseURL: URL {
        return URL.init(string: "http://news-at.zhihu.com/api/")!
    }
    //不同介面的字路徑
    var path: String {
        switch self {
        case .testApi:
            return "4/news/latest"
        case .testAPi(let para1, _):
            return "\(para1)/news/latest"
        case .testApiDict:
            return "4/news/latest"
//        default:
//            return "4/news/latest"
        }
    }
    
    /// 請求方式 get post put delete
    var method: Moya.Method {
        switch self {
        case .testApi:
            return .get
        default:
            return .post
        }
    }
    
    /// 這個是做單元測試模擬的資料,必須要實現,只在單元測試檔案中有作用
    var sampleData: Data {
        return "".data(using: String.Encoding.utf8)!
    }
    
    /// 這個就是API裡面的核心。嗯。。至少我認為是核心,因為我就被這個坑過
    //類似理解為AFN裡的URLRequest
    var task: Task {
        switch self {
        case .testApi:
            return .requestPlain
        case let .testAPi(para1, _)://這裡的缺點就是多個引數會導致parameters拼接過長
        //後臺的content-Type 為application/x-www-form-urlencoded時選擇URLEncoding            
            return .requestParameters(parameters: ["key":para1], encoding: URLEncoding.default)
        case let .testApiDict(dict)://所有引數當一個字典進來完事。
            //後臺可以接收json字串做引數時選這個
            return .requestParameters(parameters: dict, encoding: JSONEncoding.default)

        }
    }
    
    /// 設定請求頭header
    var headers: [String : String]? {
        //同task,具體選擇看後臺 有application/x-www-form-urlencoded 、application/json
        return ["Content-Type":"application/x-www-form-urlencoded"]
    }
}
複製程式碼

上面api.swift設定完畢

NetworkManager.swift

下面就開始構建我們的請求相關的東西 主要是完成對於Provider的完善及個性化設定。

首先先看一個最簡單的網路請求, 我們所有的請求都是來自於這個provider物件,測試一下 我們就能發出請求並拿到返回的結果。

        let provier = MoyaProvider<API>()
        provier.request(.testApi) { (result) in
            switch result {
            case let .success(response):
                print(response)
            case let .failure(error):
                    print("網路連線失敗")
                    break
            }
        }
複製程式碼

當然,對應情況複雜的專案這個是 遠遠不夠滴! so~ 下面開始對provider進行改造

先看看最豐滿的provider是什麼樣子的

image.png
當我看到這一個個撲朔迷離的引數時我的表情是這樣的(⊙﹏⊙)b
image.png

點進去看原始碼才發現Moya已經幫我們把每個引數都預設實現了一遍。我們可以根據自己的設計需求設定引數 每個引數什麼意思也不贅述了,Moya 的初始化  這篇文章也都說了,建議初學者閱讀一下。 ####需要指正的地方是:

image.png

文中 endpointClosure 的使用舉例中 target.parameters 已經沒有這個屬性了。現在版本的Moya用的task代替的。 Moya官方不希望在所有的請求中統一新增引數,不過我們可以自己去定義endPointClosure實現相應的效果 詳情參照:Add additional parameters to all requests 裡面有具體的解決方案。

去除了不太常用的自定義stubClosure, callbackQueue, trackInflights後我的Provider長這樣
import Foundation
import Moya
import Alamofire
import SwiftyJSON

/// 超時時長
private var requestTimeOut:Double = 30
///endpointClosure
private let myEndpointClosure = { (target: API) -> Endpoint in
///這裡的endpointClosure和網上其他實現有些不太一樣。
///主要是為了解決URL帶有?無法請求正確的連結地址的bug
    let url = target.baseURL.absoluteString + target.path
    var endpoint = Endpoint(
        url: url,
        sampleResponseClosure: { .networkResponse(200, target.sampleData) },
        method: target.method,
        task: target.task,
        httpHeaderFields: target.headers
    )
    switch target {
    case .easyRequset:
        return endpoint
    case .register:
        requestTimeOut = 5//按照專案需求針對單個API設定不同的超時時長
        return endpoint
    default:
        requestTimeOut = 30//設定預設的超時時長
        return endpoint
    }
}

private let requestClosure = { (endpoint: Endpoint, done: MoyaProvider.RequestResultClosure) in
    do {
        var request = try endpoint.urlRequest()
        //設定請求時長
        request.timeoutInterval = requestTimeOut
        // 列印請求引數
        if let requestData = request.httpBody {
            print("\(request.url!)"+"\n"+"\(request.httpMethod ?? "")"+"傳送引數"+"\(String(data: request.httpBody!, encoding: String.Encoding.utf8) ?? "")")
        }else{
            print("\(request.url!)"+"\(String(describing: request.httpMethod))")
        }
        done(.success(request))
    } catch {
        done(.failure(MoyaError.underlying(error, nil)))
    }
}

/*   設定ssl
let policies: [String: ServerTrustPolicy] = [
    "example.com": .pinPublicKeys(
        publicKeys: ServerTrustPolicy.publicKeysInBundle(),
        validateCertificateChain: true,
        validateHost: true
    )
]
*/

// 用Moya預設的Manager還是Alamofire的Manager看實際需求。HTTPS就要手動實現Manager了
//private public func defaultAlamofireManager() -> Manager {
//    
//    let configuration = URLSessionConfiguration.default
//    
//    configuration.httpAdditionalHeaders = Alamofire.SessionManager.defaultHTTPHeaders
//    
//    let policies: [String: ServerTrustPolicy] = [
//        "ap.grtstar.cn": .disableEvaluation
//    ]
//    let manager = Alamofire.SessionManager(configuration: configuration,serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies))
//    
//    manager.startRequestsImmediately = false
//    
//    return manager
//}


/// NetworkActivityPlugin外掛用來監聽網路請求
private let networkPlugin = NetworkActivityPlugin.init { (changeType, targetType) in

    print("networkPlugin \(changeType)")
    //targetType 是當前請求的基本資訊
    switch(changeType){
    case .began:
        print("開始請求網路")
        
    case .ended:
        print("結束")
    }
}

// https://github.com/Moya/Moya/blob/master/docs/Providers.md  引數使用說明
//stubClosure   用來延時傳送網路請求

let Provider = MoyaProvider<API>(endpointClosure: myEndpointClosure, requestClosure: requestClosure, plugins: [networkPlugin], trackInflights: false)
複製程式碼

NetworkManager.swift 基本寫完 還剩一點下面再說。

這個時候我們的網路請求就會長這樣:

        Provider.request(.testApi) { (result) in
            switch result {
            case let .success(response):
                print(response)
                //做相應的資料處理  這裡我用的是HandyJson
            case let .failure(error):
                print("網路連線失敗")
                //提示使用者網路連結失敗
                break
            }
        }
複製程式碼

像我這種懶得一比的開發者,當然不想每一次都寫這麼多result判斷。寫好多重複的程式碼。

image.png

於是我決定再次封裝。。。

來來,我們再次回到NetworkManager.swift 封裝provider請求。

思路:

1.後臺返回錯誤的時候我統一把errormsg顯示給使用者 2.只有返回正確的時候才把資料提取出來進行解析。 對應的網路請求的hud全部封裝到請求裡面。

這個是針對於大多數請求。個別展示效果不同的請求自己老老實實用provider.request寫就行。 下面我們在NetworkManager.swift中進行二次封裝

///先新增一個閉包用於成功時後臺返回資料的回撥
typealias successCallback = ((String) -> (Void))
///再次用一個方法封裝provider.request()
func NetWorkRequest(_ target: API, completion: @escaping successCallback ){
    //先判斷網路是否有連結 沒有的話直接返回--程式碼略
    
    //顯示hud
    Provider.request(target) { (result) in
        //隱藏hud
        switch result {
        case let .success(response):
            do {
                //這裡轉JSON用的swiftyJSON框架
                let jsonData = try JSON(data: response.data)
                //判斷後臺返回的code碼沒問題就把資料閉包返回 ,我們後臺是0000 以實際後臺約定為準。            
                if jsonData[RESULT_CODE].stringValue == "0000"{
                    completion(String(data: response.data, encoding: String.Encoding.utf8)!)
                }else{
                    //flag 不為0000 HUD顯示錯誤資訊
                    print("flag不為0000 HUD顯示後臺返回message"+"\(jsonData[RESULT_MESSAGE].stringValue)")
                }
            } catch {
            }
        case let .failure(error):
            guard let error = error as? CustomStringConvertible else {
                //網路連線失敗,提示使用者
                print("網路連線失敗")
                break
            }
        }
    }
}
複製程式碼

MoyaConfig.swift 這個就是丟一些公用字串

覺得麻煩可以放在NetworkManager.swift中 看個人愛好 程式碼如下


import Foundation
/// 定義基礎域名
let Moya_baseURL = "http://news-at.zhihu.com/api/"

/// 定義返回的JSON資料欄位
let RESULT_CODE = "flag"      //狀態碼
let RESULT_MESSAGE = "message"  //錯誤訊息提示

複製程式碼

這個時候我們再去用封裝好的網路工具優雅的進行網路請求

   NetWorkRequest(.testApi) { (response) -> (Void) in
          //用HandyJSON對返回的資料進行處理
        }
複製程式碼

github地址: github.com/Liaoworking…

個人技術部落格地址:liaoworking.com

相關文章