安裝
Moya 介紹
? Moya 是一個基于 Alamofire 的更高層網(wǎng)絡請求封裝抽象層。Moya 也就可以看做我們的網(wǎng)絡管理層,用來封裝 URL、參數(shù)等請求所需要的一些基本信息。使用后我們的客戶端代碼會直接操作Moya,然后 Moya去管理請求,而不用跟 Alamofire 進行直接接觸。
終端查找
$ pod search Moya
最新版本和資源信息
pod 'Moya', '~> 14.0.0-beta.2'
- Homepage: https://github.com/Moya/Moya
- Source: https://github.com/Moya/Moya.git
? 我們在使用Moya時,需要引入3個第三方庫,如:Moya, Alamofire, SwiftyJSON(方便解析返回的 JSON 數(shù)據(jù))
pod 'Moya', '~> 14.0.0-beta.2'
pod 'Alamofire', '~> 5.0.0-rc.2'
pod 'SwiftyJSON', '~> 5.0.0'

Moya 的特點
- 編譯時檢查正確的API端點訪問。
- 允許您定義具有關聯(lián)枚舉值的不同端點的明確用法。
- 將測試存根視為一等公民,因此單元測試非常容易。
Moya 實戰(zhàn)
接口枚舉
//初始化請求的provider
let NetProvider = MoyaProvider<NetRequestAPI>()
/**定義請求的endpoints(供provider使用)**/
public enum NetRequestAPI {
case channels //獲取頻道接口
case playlist(String) //獲取歌曲接口
case otherRequest // 其他接口,沒有參數(shù)
}
擴展封裝
//請求配置
/*
baseURL:服務器地址host 處理
path:根據(jù)不同的接口,確定各個請求的具體路徑
method:根據(jù)不同的接口,設置請求方式
headers:統(tǒng)一配置的請求頭信息配置
task:配置內部參數(shù),以及task信息
*/
extension NetRequestAPI: TargetType {
//服務器地址
public var baseURL: URL {
switch self {
case .channels:
return URL(string: "https://www.douban.com")!
case .playlist(_):
return URL(string: "https://douban.fm")!
case .otherRequest:
return URL(string: "https://douban.fm/default.html")!
}
}
// 各個請求的具體路徑
public var path: String {
switch self {
case .channels:
return "/j/app/radio/channels"
case .playlist(_):
return "/j/mine/playlist"
case .otherRequest:
return "/default/otherRequest"
}
}
//請求方式
public var method: Moya.Method {
switch self {
case .channels:
return .get
case .playlist(_):
return .get
default:
return .post
}
}
//請求任務事件(這里附帶上參數(shù))
public var task: Task {
var param: [String: Any] = [:]
switch self {
case .playlist(let channel):
param["channel"] = channel
param["type"] = "n"
param["from"] = "mainsite"
break
case .channels: break
case .otherRequest:
return .requestPlain
}
return .requestParameters(parameters: param, encoding: URLEncoding.default)
}
//是否執(zhí)行Alamofire驗證
public var validate: Bool {
return false
}
//做單元測試模擬的數(shù)據(jù),只會在單元測試文件中有作用
public var sampleData: Data {
return "{}".data(using: String.Encoding.utf8, allowLossyConversion: true)!
}
//請求頭設置
public var headers: [String : String]? {
return nil
}
}
第一種調用方式:在 Controller 中直接調用
override func viewDidLoad() {
super.viewDidLoad()
NetProvider.request(.channels) { (result) in
if case let .success(response) = result { response.data
//
let data = try? response.mapJSON() //請求response轉化為JSON格式
let json = JSON(data!) //轉化為JSON數(shù)據(jù)格式
print("channels 接口數(shù)據(jù):\(json["channels"].arrayValue)")
}
//主線程刷新UI
DispatchQueue.main.async {
// self.tableView.reloadData()
}
}
}
控制臺打?。?/p>


第二種調用方式:VM 模塊
功能 VM 模塊處理
/*
MoyaProvider 是此次網(wǎng)絡請求的信息提供者
MoyaProvider 根據(jù)模塊 NetRequestAPI 設置的信息綁定數(shù)據(jù)請求
MoyaProvider 通過調用 request 方法傳出此次請求的接口,但是參數(shù)需要應用層提供!
獲取回調信息,然后進行 json 序列化!
最后利用函數(shù)式編程思想回調 攜帶信息的閉包 給應用層
*/
//登錄模塊管理
class LoginViewModel: NSObject {
static let manager = LoginViewModel()
//驗證碼事件
func getChannel(username: String?, complete:@escaping((Any)-> Void)) {
let provider = MoyaProvider<NetRequestAPI>()
provider.request(.channels) { (result) in
switch result {
case let .success(response):
let dict = JSON(response.data)
complete(dict)
case let .failure(error):
print(error)
complete("")
}
}
}
}
-
.success(Moya.Response):成功的情況。我們可以從 Moya.Response 中得到返回數(shù)據(jù)(data)和狀態(tài)(status ); -
.failure(MoyaError):失敗的情況。這里的失敗指的是服務器沒有收到請求(例如可達性/連接性錯誤)或者沒有發(fā)送響應(例如請求超時)。我們可以在這里設置個延遲請求,過段時間重新發(fā)送請求。
? 對于.failure(error) 情況下,我們還可以通過 switch 語句判斷具體的 MoyaError 錯誤類型:
case let .failure(error):
switch error {
case .imageMapping(let response):
print("錯誤原因:\(error.errorDescription ?? "")")
print(response)
case .jsonMapping(let response):
print("錯誤原因:\(error.errorDescription ?? "")")
print(response)
case .statusCode(let response):
print("錯誤原因:\(error.errorDescription ?? "")")
print(response)
case .stringMapping(let response):
print("錯誤原因:\(error.errorDescription ?? "")")
print(response)
case .underlying(let error, let response):
print("錯誤原因:\(error.errorDescription ?? "")")
print(error)
print(response as Any)
case .requestMapping:
print("錯誤原因:\(error.errorDescription ?? "")")
print("nil")
}
過濾正確狀態(tài)碼
? 除了連接超時這樣的網(wǎng)絡問題會返回.failure,像是服務器報錯(404)、請求未授權(401)等都是返回.success 的。這樣我們還需要根據(jù)狀態(tài)碼來判斷返回數(shù)據(jù)是否是正確的。
Moya.Response 本身就提供了下面兩個方法來過濾 response 響應:
-
filterSuccessfulStatusCodes():返回狀態(tài)碼為 200 - 299 的響應 -
filterSuccessfulStatusAndRedirectCodes():返回狀態(tài)碼為 200 - 399 的響應
let provider = MoyaProvider<NetRequestAPI>()
provider.request(.channels) { (result) in
switch result {
case let .success(response):
do {
//過濾成功的狀態(tài)碼響應
try response.filterSuccessfulStatusCodes()
let data = try response.mapJSON()
let dict = JSON(data)
complete(dict)
//繼續(xù)做一些其它事情....
}
catch {
//處理錯誤狀態(tài)碼的響應...
}
case let .failure(error):
print(error)
complete("")
}
}
Controller 模擬調用
override func viewDidLoad() {
super.viewDidLoad()
//應用層只需要為此次網(wǎng)絡提供信息參數(shù)
//在回調閉包拿到信息,處理其他業(yè)務就OK!
//VM層調用
LoginViewModel.manager.getChannel(username: nil) { [weak self](responseData) in
//self?.smscodeTF.text = smscode
print("Controller調用:\(responseData)")
}
}
打印圖:

封裝一個請求/結果處理的適配器
? 根據(jù)項目需求我們可以自行定義一個適配器(adapter),將請求和結果的判斷處理都封裝起來。然后通過三個回調函數(shù)返回相應的結果,這三個回調函數(shù)分別對應三種響應情況:
-
success:服務器連接成功,且數(shù)據(jù)正確返回(同時會自動將數(shù)據(jù)轉換成 JSON 對象,方便使用) -
error:服務器連接成功,但數(shù)據(jù)返回錯誤(同時會返回錯誤信息) -
failure :服務器連接不上,網(wǎng)絡異常等(同時會返回錯誤信息。必要的話,還可以在此增加自動重新請求的機制。)
struct Network {
static let provider = MoyaProvider<DouBan>()
static func request(
_ target: DouBan,
success successCallback: @escaping (JSON) -> Void,
error errorCallback: @escaping (Int) -> Void,
failure failureCallback: @escaping (MoyaError) -> Void
) {
provider.request(target) { result in
switch result {
case let .success(response):
do {
//如果數(shù)據(jù)返回成功則直接將結果轉為JSON
try response.filterSuccessfulStatusCodes()
let json = try JSON(response.mapJSON())
successCallback(json)
}
catch let error {
//如果數(shù)據(jù)獲取失敗,則返回錯誤狀態(tài)碼
errorCallback((error as! MoyaError).response!.statusCode)
}
case let .failure(error):
//如果連接異常,則返滬錯誤信息(必要時還可以將嘗試重新發(fā)起請求)
//if target.shouldRetry {
// retryWhenReachable(target, successCallback, errorCallback,
// failureCallback)
//}
//else {
failureCallback(error)
//}
}
}
}
}
Moya模型總結:CFNextwork -> Alamofire -> Moya -> 業(yè)務層

弊端:
- 若整個項目是
RxSwift(畢竟現(xiàn)在函數(shù)響應式編程已成為趨勢),顯然我們更需要序列,因為序列可以直接綁定響應UI,更便于開發(fā); - 當前直接使用 Moya ,還缺了很嚴重的一步即:模型化。
RxSwift 結合 Aoya
真實的網(wǎng)絡架構層的模塊希望是下面這樣的:
Moya模型總結:CFNextwork -> Alamofire -> Moya -> 模型化 -> RxSwift -> 業(yè)務層

首先要使用CocoaPods 在 Podfile 文件中進行如下配置:
pod 'Moya', '~> 14.0.0-beta.2'
pod 'Alamofire', '~> 5.0.0-rc.2'
pod 'SwiftyJSON', '~> 5.0.0'
pod 'ObjectMapper', '~> 3.5.1'
導入文件:
@_exported import Alamofire
@_exported import SwiftyJSON
@_exported import ObjectMapper
實戰(zhàn)代碼如下:
/*
首先拓展 PrimitiveSequence 實際對象是處理 Moya.Response
通過調用 SwiftyJSON 把 Response 的 data 解析成 json
然后調用 ObjectMapper 轉成相應模型數(shù)據(jù)
數(shù)組模型處理差不多,大家只要返回 [T] 就 OK
*/
extension PrimitiveSequence where Trait == SingleTrait, Element == Moya.Response {
func map<T: ImmutableMappable>(_ type: T.Type) -> PrimitiveSequence<TraitType, T> {
return self
.map { (response) -> T in
let json = try JSON(data: response.data)
guard let code = json[RESULT_CODE].int else { throw RequestError.noCodeKey }
if code != StatusCode.success.rawValue { throw RequestError.sysError(statusCode:"\(code)" , errorMsg: json[RESULT_MESSAGE].string) }
if let data = json[RESULT_DATA].dictionaryObject {
return try Mapper<T>().map(JSON: data)
}else if let data = json[RESULT_RESULT].dictionaryObject {
return try Mapper<T>().map(JSON: data)
}
throw RequestError.noDataKey
}.do(onSuccess: { (_) in
}, onError: { (error) in
if error is MapError {
log.error(error)
}
})
}
}
外界調用
loginService.login().asObservable()
.subscribe(onNext: {[weak self] (rcmdBranchModel) in
guard let `self` = self else { return }
self.requestIds = rcmdBranchModel.tab.map{$0.id}
self.menuTitles += rcmdBranchModel.tab.map{$0.name}
self.pageController.magicView.reloadData(toPage: 1)
}).disposed(by: disposeBag)
參考資料
網(wǎng)絡抽象層庫Moya的使用詳解4
Moya網(wǎng)絡層框架的學習筆記 US
Moya使用理解 US
moya的使用 US
系統(tǒng)性學習Moya+Alamofire+RxSwift+ObjectMapper的配合使用 US