-------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ò)框架。
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文件
我們大致可將網(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)來的文藝寫法。。
直接點(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是什么樣子的

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

點(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 的初始化 這篇文章也都說了。
上文需要指正的地方是:

文中 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ù)的代碼。

于是我決定再次封裝。。。
來來,我們?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è)人需求了~