踩坑踩了4天總算把基於Moya的網路框架搭建完畢
看網上關於Moya的教程不太多,大多都是一樣的,還有一些年久失修。這裡專門講講關於moya的搭建及容易遇到的一些坑。
重要的東西放到最前面
1.最好的教材是官方文件和Demo,Moya有中文文件。
2.嘗試一些不一樣的東西會讓開發更有趣。
3.我把Demo地址放最後了。
為什麼選擇moya:
一開始網路框架的選型有Alamofire和Moya。
Alamofire可以說是Swift版本的AFN,啃AFN的老啃了幾年了,AFN的確博大精深,有很多值得開發者去學校的地方。但開發這麼多年,AFN實在是啃不動了。試著封裝了一下Alamofire。感覺和AFN封裝大同小異。
和技術群裡的一些大佬討論了一下,大多數也是推薦Moya,至於聊天記錄裡面提及的 包含?地址的問題 我們在稍後的內容裡去解決。後來咬咬牙就決定使用Moya用新專案的網路框架。
About Moya
已經有大神把Moya的基本使用和各個模組的介紹說的很清楚了,這裡就不贅述了,建議把框架的基本使用瞭解一番【iOS開發】Moya入坑記-用法解讀篇
上文作為入門是一篇不錯的文章,但作為實際開發過程中,健壯全方位考慮的網路框架來說的來說還有很多用法並沒有提及。 而且網上很多文章都是老版本,看的時候會感覺有些懵。。。所以我就寫了本文?
Let's Begin
####封裝的目錄結構
安裝好Moya後我們建立好三個空的Swift檔案
我們大致可將網路框架拆分成
API.swift---將來我們的介面列表和不同的介面的一些配置在裡面完成,最長打交道的地方。
NetworkManager.swift---基本框架配置及封裝寫到這裡
MoyaConfig.swift---這個其實可有可無的,習慣上把baseURL和一些公用字串放進來
OK我們正式開始coding!
API.swift中先建立一個API的列舉,列舉值是介面名, 並建立遵守TargetType協議的extention。
這裡我寫三個測試的Api。第一個是無參,第二個是普通寫法(我看官方文件好像是這種 多引數 都寫進去的,實際開發過程中感覺有些麻煩),第三個是直接把所有引數包裝成字典傳進來的文藝寫法。。
直接點選錯誤程式碼補全即可自動補全所有的協議
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是什麼樣子的
當我看到這一個個撲朔迷離的引數時我的表情是這樣的(⊙﹏⊙)b點進去看原始碼才發現Moya已經幫我們把每個引數都預設實現了一遍。我們可以根據自己的設計需求設定引數 每個引數什麼意思也不贅述了,Moya 的初始化 這篇文章也都說了,建議初學者閱讀一下。 ####需要指正的地方是:
文中 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判斷。寫好多重複的程式碼。
於是我決定再次封裝。。。
來來,我們再次回到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