[iOS開發(fā)] 超詳細(xì)-swift Moya+handyJSON網(wǎng)絡(luò)框架的搭建及封裝

-------2024.05.11 update----

最近換了新的公司,公司的項(xiàng)目比較新兼容的版本也比較高,使用的Codable進(jìn)行的JSON模型轉(zhuǎn)換。

隨著HandyJSON 放棄維護(hù)和 Codable 日益強(qiáng)大

最新版的Demo JSON轉(zhuǎn)模型使用Codable進(jìn)行轉(zhuǎn)換。

移除了HandyJSON 和 ObjectMapper

如仍然需要ObjectMapper Demo的小伙伴可以切換到feature/ObjectMapper分支查看

-------2021.03.11 update----

Moya已經(jīng)使用有3年了,但之前的封裝總感覺欠缺點(diǎn)什么,要么就是寫起來不夠優(yōu)美,要么部分地方感覺隆昌的冗余。

后來和同事一起討論怎么才算優(yōu)美的網(wǎng)絡(luò)請(qǐng)求的封裝, 總結(jié)了一些特性之后,再利用Swift的泛型和重載,對(duì)現(xiàn)有的網(wǎng)絡(luò)框架就行了最終的改造。
現(xiàn)在看起來和想象的處理方式差不多了。
代碼已經(jīng)更新到Demo中。
核心代碼在NetworkManager.swift文件中
業(yè)務(wù)調(diào)用示例代碼在ViewController.swift文件中

-------2020.09.17 update----

前一段時(shí)間網(wǎng)絡(luò)框架優(yōu)化,隨著業(yè)務(wù)模塊變復(fù)雜,發(fā)現(xiàn)現(xiàn)有Api接口的文件已經(jīng)有一千行左右。迫不得已在原有的基礎(chǔ)上做模塊區(qū)分。

具體的拆分可以在Demo中查看多業(yè)務(wù)模塊的拆分文件夾,網(wǎng)絡(luò)請(qǐng)求的封裝部分邏輯基本不變。

-------2020.03.07 update----

經(jīng)過我?guī)啄甑捻?xiàng)目實(shí)踐,HandyJSON庫是真的香,JSON轉(zhuǎn)模型,方便。 但是有一點(diǎn)不得不提一下,就是HandyJSON 穩(wěn)定性相關(guān)的一些問題??????,Swift5.0 的時(shí)候出過一個(gè)泛型解析失敗的bug,后來修好了,iOS13.4 beta 的時(shí)候由于Swift改動(dòng)底層源碼 導(dǎo)致HandyJSON崩潰。 因?yàn)檫@個(gè)問題我們公司的項(xiàng)目專門發(fā)了一個(gè)bug fixed版本。 從穩(wěn)定性的角度可以用業(yè)界比較多的SwiftJSON + Codable 或者ObjectMapper 來做JSON轉(zhuǎn)模型。
本Demo和文章中網(wǎng)絡(luò)框架的解析和封裝都比較穩(wěn)定 可以盡情使用

------ 2019.11.24 update 新增了另外一種封裝思路,寫在最后,下面的是正文。------

踩坑踩了4天總算把基于Moya的網(wǎng)絡(luò)框架搭建完畢

看網(wǎng)上關(guān)于Moya的教程不太多,大多都是一樣的,還有一些年久失修。這里專門講講關(guān)于moya的搭建及容易遇到的一些坑。

重要的東西放到最前面

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

2.嘗試一些不一樣的東西會(huì)讓開發(fā)更有趣。

3.寫案例不給Demo不太好吧。我把Demo地址放到最后了。

為什么選擇moya:

一開始網(wǎng)絡(luò)框架的選型有Alamofire和Moya。

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

和技術(shù)群里的一些大佬討論了一下,大多數(shù)也是推薦Moya,至于聊天記錄里面提及的 包含?地址的問題 我們?cè)谏院蟮膬?nèi)容里去解決。后來咬咬牙就決定使用Moya用新項(xiàng)目的網(wǎng)絡(luò)框架。

image

About Moya

已經(jīng)有大神把Moya的基本使用和各個(gè)模塊的介紹說的很清楚了,這里就不贅述了,建議把框架的基本使用了解一番【iOS開發(fā)】Moya入坑記-用法解讀篇

上文作為入門是一篇不錯(cuò)的文章,但作為實(shí)際開發(fā)過程中,健壯全方位考慮的網(wǎng)絡(luò)框架來說的來說還有很多用法并沒有提及。 而且網(wǎng)上很多文章都是老版本,看的時(shí)候會(huì)感覺有些懵。。。所以我就寫了本文??

Let's Begin

封裝的目錄結(jié)構(gòu)

安裝好Moya后我們 創(chuàng)建好三個(gè)空的Swift文件

image

我們大致可將網(wǎng)絡(luò)框架拆分成

API.swift ---將來我們的接口列表和不同的接口的一些配置在里面完成,最長(zhǎng)打交道的地方。

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

MoyaConfig.swift ---這個(gè)其實(shí)可有可無的,習(xí)慣上把baseURL和一些公用字符串放進(jìn)來

OK我們正式開始coding!

API.swift中先創(chuàng)建一個(gè)API的枚舉,枚舉值是接口名, 并創(chuàng)建遵守TargetType協(xié)議的extention。

這里我寫三個(gè)測(cè)試的Api。第一個(gè)是無參,第二個(gè)是普通寫法(我看官方文檔好像是這種 多參數(shù) 都寫進(jìn)去的,實(shí)際開發(fā)過程中感覺有些麻煩),第三個(gè)是直接把所有參數(shù)包裝成字典傳進(jìn)來的文藝寫法。。

image

直接點(diǎn)擊 錯(cuò)誤代碼補(bǔ)全 即可自動(dòng)補(bǔ)全所有的協(xié)議

import Foundation
import Moya

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

extension API:TargetType{
    
    //baseURL 也可以用枚舉來區(qū)分不同的baseURL,不過一般也只有一個(gè)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"
        }
    }
    
    /// 請(qǐng)求方式 get post put delete
    var method: Moya.Method {
        switch self {
        case .testApi:
            return .get
        default:
            return .post
        }
    }
    
    /// 這個(gè)是做單元測(cè)試模擬的數(shù)據(jù),必須要實(shí)現(xiàn),只在單元測(cè)試文件中有作用
    var sampleData: Data {
        return "".data(using: String.Encoding.utf8)!
    }
    
    /// 這個(gè)就是API里面的核心。嗯。。至少我認(rèn)為是核心,因?yàn)槲揖捅贿@個(gè)坑過
    //類似理解為AFN里的URLRequest
    var task: Task {
        switch self {
        case .testApi:
            return .requestPlain
        case let .testAPi(para1, _)://這里的缺點(diǎn)就是多個(gè)參數(shù)會(huì)導(dǎo)致parameters拼接過長(zhǎng)
        //后臺(tái)的content-Type 為application/x-www-form-urlencoded時(shí)選擇URLEncoding            
            return .requestParameters(parameters: ["key":para1], encoding: URLEncoding.default)
        case let .testApiDict(dict)://所有參數(shù)當(dāng)一個(gè)字典進(jìn)來完事。
            //后臺(tái)可以接收json字符串做參數(shù)時(shí)選這個(gè)
            return .requestParameters(parameters: dict, encoding: JSONEncoding.default)

        }
    }
    
    /// 設(shè)置請(qǐng)求頭header
    var headers: [String : String]? {
        //同task,具體選擇看后臺(tái) 有application/x-www-form-urlencoded 、application/json
        return ["Content-Type":"application/x-www-form-urlencoded"]
    }
}

上面api.swift設(shè)置完畢

NetworkManager.swift

下面就開始構(gòu)建我們的請(qǐng)求相關(guān)的東西
主要是完成對(duì)于Provider的完善及個(gè)性化設(shè)置。

首先先看一個(gè)最簡(jiǎn)單的網(wǎng)絡(luò)請(qǐng)求, 我們所有的請(qǐng)求都是來自于這個(gè)provider對(duì)象,測(cè)試一下 我們就能發(fā)出請(qǐng)求并拿到返回的結(jié)果。

注: 在2020.09.17下載的Demo中 provier 的對(duì)象的創(chuàng)建MoyaProvider<API>已經(jīng)替換成了MoyaProvider<MultiTarget(對(duì)多業(yè)務(wù)API情況的封裝)>包裝好的枚舉體,用以多業(yè)務(wù)的拆分。 具體可參考demo.
        let provier = MoyaProvider<API>()
        provier.request(.testApi) { (result) in
            switch result {
            case let .success(response):
                print(response)
            case let .failure(error):
                    print("網(wǎng)絡(luò)連接失敗")
                    break
            }
        }

當(dāng)然,對(duì)應(yīng)情況復(fù)雜的項(xiàng)目這個(gè)是 遠(yuǎn)遠(yuǎn)不夠滴!

so~ 下面開始對(duì)provider進(jìn)行改造

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


image.png

當(dāng)我看到這一個(gè)個(gè)撲朔迷離的參數(shù)時(shí)我的表情是這樣的(⊙﹏⊙)b

image.png

點(diǎn)進(jìn)去看源碼才發(fā)現(xiàn)Moya已經(jīng)幫我們把每個(gè)參數(shù)都默認(rèn)實(shí)現(xiàn)了一遍。我們可以根據(jù)自己的設(shè)計(jì)需求設(shè)置參數(shù)
每個(gè)參數(shù)什么意思也不贅述了,Moya 的初始化 這篇文章也都說了。

上文需要指正的地方是:

image.png

文中 endpointClosure 的使用舉例中 target.parameters 已經(jīng)沒有這個(gè)屬性了?,F(xiàn)在版本的Moya用的task代替的。
Moya官方不希望在所有的請(qǐng)求中統(tǒng)一添加參數(shù),不過我們可以自己去定義endPointClosure實(shí)現(xiàn)相應(yīng)的效果
詳情參照:Add additional parameters to all requests 里面有具體的解決方案。

根據(jù)實(shí)際項(xiàng)目需求去除了不太常用的 stubClosure , callbackQueue , trackInflights 后我的Provider長(zhǎng)這樣

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

下面我們就開始動(dòng)手構(gòu)建我們的networkManager

import Foundation
import Moya
import Alamofire
import SwiftyJSON

/// 超時(shí)時(shí)長(zhǎng)
private var requestTimeOut:Double = 30
///endpointClosure
private let myEndpointClosure = { (target: API) -> Endpoint in
///這里的endpointClosure和網(wǎng)上其他實(shí)現(xiàn)有些不太一樣。
///主要是為了解決URL帶有?無法請(qǐng)求正確的鏈接地址的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//按照項(xiàng)目需求針對(duì)單個(gè)API設(shè)置不同的超時(shí)時(shí)長(zhǎng)
        return endpoint
    default:
        requestTimeOut = 30//設(shè)置默認(rèn)的超時(shí)時(shí)長(zhǎng)
        return endpoint
    }
}

private let requestClosure = { (endpoint: Endpoint, done: MoyaProvider.RequestResultClosure) in
    do {
        var request = try endpoint.urlRequest()
        //設(shè)置請(qǐng)求時(shí)長(zhǎng)
        request.timeoutInterval = requestTimeOut
        // 打印請(qǐng)求參數(shù)
        if let requestData = request.httpBody {
            print("\(request.url!)"+"\n"+"\(request.httpMethod ?? "")"+"發(fā)送參數(shù)"+"\(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)))
    }
}

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

// 用Moya默認(rèn)的Manager還是Alamofire的Manager看實(shí)際需求。HTTPS就要手動(dòng)實(shí)現(xiàn)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插件用來監(jiān)聽網(wǎng)絡(luò)請(qǐng)求
private let networkPlugin = NetworkActivityPlugin.init { (changeType, targetType) in

    print("networkPlugin \(changeType)")
    //targetType 是當(dāng)前請(qǐng)求的基本信息
    switch(changeType){
    case .began:
        print("開始請(qǐng)求網(wǎng)絡(luò)")
        
    case .ended:
        print("結(jié)束")
    }
}

// https://github.com/Moya/Moya/blob/master/docs/Providers.md  參數(shù)使用說明
//stubClosure   用來延時(shí)發(fā)送網(wǎng)絡(luò)請(qǐng)求

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

NetworkManager.swift 基本寫完 還剩一點(diǎn)下面再說。

這個(gè)時(shí)候我們的網(wǎng)絡(luò)請(qǐng)求就會(huì)長(zhǎng)這樣:

        Provider.request(.testApi) { (result) in
            switch result {
            case let .success(response):
                print(response)
                //做相應(yīng)的數(shù)據(jù)處理  這里我用的是HandyJson
            case let .failure(error):
                print("網(wǎng)絡(luò)連接失敗")
                //提示用戶網(wǎng)絡(luò)鏈接失敗
                break
            }
        }

像我這種懶得一比的開發(fā)者,當(dāng)然不想每一次都寫這么多result判斷。寫好多重復(fù)的代碼。

image.png

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

來來,我們?cè)俅位氐絅etworkManager.swift 封裝provider請(qǐng)求。

思路:

1.后臺(tái)返回錯(cuò)誤的時(shí)候我統(tǒng)一把error msg顯示給用戶

2.只有返回正確的時(shí)候才把數(shù)據(jù)提取出來進(jìn)行解析。 對(duì)應(yīng)的網(wǎng)絡(luò)請(qǐng)求的hud全部封裝到請(qǐng)求里面。

這個(gè)是針對(duì)于大多數(shù)請(qǐng)求。個(gè)別展示效果不同的請(qǐng)求自己老老實(shí)實(shí)用provider.request寫就行。
下面我們?cè)贜etworkManager.swift中進(jìn)行二次封裝

///先添加一個(gè)閉包用于成功時(shí)后臺(tái)返回?cái)?shù)據(jù)的回調(diào)
typealias successCallback = ((String) -> (Void))
///再次用一個(gè)方法封裝provider.request()
func NetWorkRequest(_ target: API, completion: @escaping successCallback ){
    //先判斷網(wǎng)絡(luò)是否有鏈接 沒有的話直接返回--代碼略
    
    //顯示hud
    Provider.request(target) { (result) in
        //隱藏hud
        switch result {
        case let .success(response):
            do {
                //這里轉(zhuǎn)JSON用的swiftyJSON框架
                let jsonData = try JSON(data: response.data)
                //判斷后臺(tái)返回的code碼沒問題就把數(shù)據(jù)閉包返回 ,我們后臺(tái)是0000 以實(shí)際后臺(tái)約定為準(zhǔn)。            
                if jsonData[RESULT_CODE].stringValue == "0000"{
                    completion(String(data: response.data, encoding: String.Encoding.utf8)!)
                }else{
                    //flag 不為0000 HUD顯示錯(cuò)誤信息
                    print("flag不為0000 HUD顯示后臺(tái)返回message"+"\(jsonData[RESULT_MESSAGE].stringValue)")
                }
            } catch {
            }
        case let .failure(error):
            guard let error = error as? CustomStringConvertible else {
                //網(wǎng)絡(luò)連接失敗,提示用戶
                print("網(wǎng)絡(luò)連接失敗")
                break
            }
        }
    }
}

MoyaConfig.swift 這個(gè)就是放一些公用字符串

覺得麻煩可以放在NetworkManager.swift中 看個(gè)人愛好
代碼如下


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

/// 定義返回的JSON數(shù)據(jù)字段
let RESULT_CODE = "flag"      //狀態(tài)碼
let RESULT_MESSAGE = "message"  //錯(cuò)誤消息提示

這個(gè)時(shí)候我們?cè)偃ビ梅庋b好的網(wǎng)絡(luò)工具優(yōu)雅的進(jìn)行網(wǎng)絡(luò)請(qǐng)求

   NetWorkRequest(.testApi) { (response) -> (Void) in
          //用HandyJSON對(duì)返回的數(shù)據(jù)進(jìn)行處理
        }

------------- 2019.11.24 update ↓ -----------

兩年前我寫了這篇關(guān)于Moya網(wǎng)絡(luò)框架的封裝的文章,

上面的封裝思路的原則是能少寫代碼就少寫代碼。懶人專用。

隨著業(yè)務(wù)的發(fā)展 API 文件中的switch case 文件越來越多。 其實(shí)個(gè)人感覺維護(hù)起來其實(shí)也還好。

最近打算再次優(yōu)化,把不同模塊的API封裝到不同的 枚舉enum 中,
這個(gè)時(shí)候遇到了一個(gè)問題 就是上面的Provider只能用于API這個(gè)枚舉體的數(shù)據(jù)

如果要新寫新的枚舉體,要封裝一套新的Provider了。 后來查看了一些國外開發(fā)者對(duì)Moya的封裝。 有一部分是把不同模塊的API封裝到不同的枚舉中去維護(hù)。 然后針對(duì)于不同的模塊去創(chuàng)建Provider類,并內(nèi)部對(duì)Provider 做具體的實(shí)現(xiàn)。

使用的時(shí)候 使用具體的Provider類的實(shí)例去做網(wǎng)絡(luò)請(qǐng)求。

這樣的好處是可以分開管理不同的模塊(其實(shí)Moya的初衷就是取抽離網(wǎng)絡(luò)請(qǐng)求和具體的業(yè)務(wù)邏輯, 已經(jīng)有一點(diǎn)解耦的意思了)。 壞處就是代碼量會(huì)稍微多一些。

具體的代碼實(shí)現(xiàn)我也寫了Demo放在的項(xiàng)目里面。

真正喜歡用哪個(gè)就看個(gè)人需求了~

Demo地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 陽光的女孩都討人喜歡 并不是為了別人喜歡而變的陽光 而是為了自己開心而變的陽光 剛好,大家都喜歡你陽光的樣子 我喜...
    藝伙閱讀 375評(píng)論 1 1
  • 閑暇時(shí)光最愜意的事,就是帶著女兒去小區(qū)周邊溜達(dá),北京話叫遛彎兒。眼看著女兒從襁褓中的嬰兒到現(xiàn)在能跳能跑,還能呀...
    文案絕學(xué)閱讀 374評(píng)論 0 0
  • 一江秋色一江風(fēng),一場(chǎng)繁華一場(chǎng)空。 雁去衡陽留不住,猶聞布谷喚歸聲。
    慶善閱讀 767評(píng)論 4 14

友情鏈接更多精彩內(nèi)容