提及來汗顏。git
最近項目纔開始使用 Swift
語言,正如我一個朋友嘲笑的:咱們都快用爛的東西大家纔開始用 ,我當時竟無言以對。github
那既然用了 Swift
,就要想辦法用舒服,用明白。從 OC
工程轉換到 Swift
工程,OC 的一些庫,好比:網絡請求庫(AFNetworking),Json解析(YYModel), 響應式編程(RAC),還有網絡請求的封裝庫(本身封裝的或者第三方的) 就要按需更換了。編程
毫無疑問是 Alamofire 了,就和 OC 項目選擇 AFNetworking 同樣。json
Swift
也有很多,好比 SwiftyJSON,HandyJSON 等。swift
SwiftyJSON
很是強大,能幫助開發者將 Json 轉成字典,按照 key 值取出時也幫助開發者進行路徑判空,可是,我我的感受用起來有點奇怪。api
後來選擇了阿里的 HandyJSON,HandyJSON
也支持結構體,支持將 Json
轉成對象,支持模型數組,由於 Swift
上對泛型的支持,因此對比 OC
上的 YYModel
用起來更舒服些。數組
Swift
是靜態語言,採用鏈式函數編程,Swift
中使用響應式編程,會讓 Swift
更加簡單和輕巧。服務器
目前能夠選擇有不少,好比 ReactiveCocoa(Swift),RxSwift,Swift Combine(蘋果本身的)
,各有優勢缺點,各位客官能夠自由比對選擇,若是第一次接觸的話,就本身隨意選一個(畢竟使用過了才能對比)。markdown
RxSwift 維護人員較多,這意味着你能輕易找到問題的解決方案,而且 RxSwift
是 ReactiveX
的一個而已,它還有 RxJava
,RxPython
等等。學會了一個,說不定其餘都是同樣哦。網絡
ReactiveCocoa(Swift),這個是從 OC 上翻譯過來的,有一些歷史的 OC 包袱,可是原來熟悉 RAC 的會更容易上手。
Swift Combine
是蘋果本身的,本身的親兒子,將來更新的概率會更大,而且不會出現第三庫不在維護更新的。
若是大家公司 OC 項目上,有在網絡庫上再次封裝的好用、強大的庫,那麼這個你就不用看了,你確定只能混編。
對於以前本身項目只有簡單再封裝 AFNetworking 或者是新項目的,推薦使用 Moya。
Moya
只是對 Alamofire
的再次封裝,並非網絡請求庫,因此使用 Moya
就須要使用 Alamofire
。
既然是網絡庫的再次封裝,那麼就能夠將
Alamofire
替換成其餘的,只須要重寫Moya+Alamofire.swift
就能夠了。我我的感受通常不必。
Moya 是對 Alamofire
的再封裝,若是隻是使用的話,關心 Moya
的使用方法便可。
Moya 分別提供了Moya英文文檔 和 Moya中文文檔。(英文文檔更全面)
下載官方的 Demo 後,先熟悉一下 Moya
的用法。
文檔已經很詳細,這裏簡單說明一下
/// 建立一個文件 MyService.swift
/// 聲明一個枚舉
enum MyService {
/// 分類放置你的請求調用函數
case createUser(firstName: String, lastName: String) } /// 擴展你的枚舉,遵照 TargetType 協議 extension MyService: TargetType {
var baseURL: {
/// 放入 host
return baseURL;
}
var path: String {
case createUser(let firstName, let lastName) /// 返回具體請求路徑 return "/user/create/user" } var method: Moya.Method {
switch self {
case .createUser:
/// 返回 .get 或者 .post
return .post;
}
}
var task: Task {
switch self {
case .createUser(let firstName, let lastName):
/// 具體請求參數
return .requestParameters(parameters: ["first_name": firstName, "last_name": lastName], encoding: JSONEncoding.default)
}
}
var sampleData: Data {
/// 若是服務器給了測試示例,能夠放到這裏
case .createUser(let firstName, let lastName):
return "{\"id\": 100, \"first_name\": \"\(firstName)\", \"last_name\": \"\(lastName)\"}".utf8Encoded
}
var headers: [String: String]? {
/// 請求頭設置
return ["Content-type": "application/json"]
}
}
複製代碼
而後你就能夠在你的 ViewController 調用了:
let provider = MoyaProvider<MyService>()
provider.request(.createUser(firstName: "James", lastName: "Potter")) { result in
// do something with the result (read on for more details)
}
// The full request will result to the following:
// POST https://api.myservice.com/users
// Request body:
// {
// "first_name": "James",
// "last_name": "Potter"
// }
複製代碼
上面只是初步使用了一下 Moya
,可是具體業務遠比 Demo 複雜的多,Moya 也給咱們提供至關充足的施展空間。
第一步仍是建立一個文件,聲明一個枚舉,實現 TargetType
協議。可是建立 MoyaProvider
對象就不一樣了。
上方代碼只是使用了 let provider = MoyaProvider<MyService>()
建立,其實 MoyaProvider
中還有其餘參數的。具體來看一下:
/// Initializes a provider.
public init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping, requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping, stubClosure: @escaping StubClosure = MoyaProvider.neverStub, callbackQueue: DispatchQueue? = nil, manager: Manager = MoyaProvider<Target>.defaultAlamofireManager(), plugins: [PluginType] = [], trackInflights: Bool = false) {
self.endpointClosure = endpointClosure
self.requestClosure = requestClosure
self.stubClosure = stubClosure
self.manager = manager
self.plugins = plugins
self.trackInflights = trackInflights
self.callbackQueue = callbackQueue
}
/// Returns an `Endpoint` based on the token, method, and parameters by invoking the `endpointClosure`.
open func endpoint(_ token: Target) -> Endpoint {
return endpointClosure(token)
}
複製代碼
這裏看到 MoyaProvider
對象 init
的時候還額外提供了 7 個參數,只是若是你使用了默認的 init
,其餘會被自動賦上默認值。
默認源碼以下:
final class func defaultEndpointMapping(for target: Target) -> Endpoint {
return Endpoint(
url: URL(target: target).absoluteString,
sampleResponseClosure: { .networkResponse(200, target.sampleData) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers
)
}
複製代碼
這裏是將建立的遵照協議的枚舉 MyService
轉化成 Endpoint
,每每咱們只是使用它的默認方法。 查閱 Endpoint
,發現還提供了兩個方法:
open func adding(newHTTPHeaderFields: [String: String]) -> Endpoint
:用於更改請求頭。
open func replacing(task: Task) -> Endpoint
: 將原有 MyService
枚舉中實現的 task
進行替換。
可是有時候也有業務測試的需求,如:網絡錯誤,超時等。就能夠在這裏實現。
Moya官方解釋:因爲它是一個閉包, 它將在每次調用API時被執行, 因此你能夠作任何你想要的操做。
Moya
給了一個例子,只須要將對象 failureEndpointClosure
傳入 MoyaProvider
的參數endpointClosure
便可。
let failureEndpointClosure = { (target: MyService) -> Endpoint in
let sampleResponseClosure = { () -> (EndpointSampleResponse) in
if shouldTimeout {
return .networkError(NSError())
} else {
return .networkResponse(200, target.sampleData)
}
}
return Endpoint(url: URL(target: target).absoluteString,
sampleResponseClosure: sampleResponseClosure,
method: target.method,
task: target.task)
}
複製代碼
這裏能夠將 MyService
轉化成 Endpoint
對象的時候能夠任意改變參數,知足各類測試需求。
根據 Endpoint
生成 URLRequest
。
默認源碼以下:
final class func defaultRequestMapping(for endpoint: Endpoint, closure: RequestResultClosure) {
do {
let urlRequest = try endpoint.urlRequest()
closure(.success(urlRequest))
} catch MoyaError.requestMapping(let url) {
closure(.failure(MoyaError.requestMapping(url)))
} catch MoyaError.parameterEncoding(let error) {
closure(.failure(MoyaError.parameterEncoding(error)))
} catch {
closure(.failure(MoyaError.underlying(error, nil)))
}
}
複製代碼
代碼中看到,經過 let urlRequest = try endpoint.urlRequest()
方式由 Endpoint
生成一個 URLRequest
對象,就意味着能夠修改 URLRequest
中的參數,好比須要給 URLRequest
設置 timeoutInterval
等。
示例以下:
let requestClosure = { (endpoint: Endpoint, done: MoyaProvider.RequestResultClosure) in
do {
var request: URLRequest = try endpoint.urlRequest()
request.httpShouldHandleCookies = false
request.timeoutInterval = 15
done(.success(request))
} catch {
done(.failure(MoyaError.underlying(error, nil)))
}
}
複製代碼
這個參數提供了3個枚舉:
.never
(默認的):直接請求服務器;
.immediate
:走協議中 sampleData
示例數據;
.delayed(seconds)
能夠把 stub 請求延遲指定時間,例如, .delayed(0.2)
能夠把每一個 stub 請求延遲 0.2s 。 這個在單元測試中來模擬網絡請求是很是有用的。
官方示例:
let stubClosure = { target: MyService -> Moya.StubBehavior in
switch target {
/* Return something different based on the target. */
}
}
複製代碼
回調線程。
這裏直接使用官方解釋了,大多工程這裏都用默認的。
接下來就是 session 參數,默認會得到一個經過基本配置進行初始化的自定義的 Alamofire.Session 實例對象
final class func defaultAlamofireSession() -> Session {
let configuration = URLSessionConfiguration.default
configuration.headers = .default
return Session(configuration: configuration, startRequestsImmediately: false)
}
複製代碼
這兒只有一個須要注意的事情:因爲在 AF 中建立一個 Alamofire.Request
對象時默認會當即觸發請求,即便爲單元測試進行 "stubbing" 請求也同樣。 所以在Moya中, startRequestsImmediately
屬性被默認設置成了 false
。
若是你須要自定義本身的 session , 好比說建立一個 SSL pinning
而且添加到 session
中,全部請求將經過自定義配置的 session
進行路由。
let serverTrustManager = ServerTrustManager(evaluators: ["example.com": PinnedCertificatesTrustEvaluator()])
let session = Session(
configuration: configuration,
startRequestsImmediately: false,
serverTrustManager: serverTrustManager
)
let provider = MoyaProvider<MyTarget>(session: session)
複製代碼
plugins
是一個攔截器數組,能夠傳入多個遵照 PluginType
協議的對象。查閱 PluginType
協議:
/// - inject additional information into a request
public protocol PluginType {
/// Called to modify a request before sending.
/// requestClosure 生成 URLRequest 生成以後回調此方法
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest /// Called immediately before a request is sent over the network (or stubbed). /// 網絡請求發出前回調此方法 func willSend(_ request: RequestType, target: TargetType) /// Called after a response has been received, but before the MoyaProvider has invoked its completion handler. /// 收到數據,Moya 尚未進行處理是回調此方法 func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) /// Called to modify a result before completion. /// 在網絡 callBack 閉包回調前回調此方法 func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError> } 複製代碼
這裏能乾的事情太多。
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest
方法回調後,能夠將公共參數(版本號,token,userid)進行拼接,或者對數據進行 RSA
加密加簽。舉個 🌰 :
/// Called to modify a request before sending.
public func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
/// 這裏作公共參數
let target = target as! MyService
var parameters : [String: Any]?
if let requstData = request.httpBody {
do {
let json = try JSONSerialization.jsonObject(with: requstData, options: .mutableContainers)
parameters = json as? [String: Any]
} catch {
/// 失敗處理 ...
}
} else {
parameters = [String: Any]()
}
/// 拼接公共參數
parameters = paramsForPublicParmeters(parameters: parameters)
/// 加密加簽
parameters = RSA.sign(withParamDic: parameters)
do {
/// 替換 httpBody
if let parameters = parameters {
return try request.encoded(parameters: parameters, parameterEncoding: JSONEncoding.default)
}
} catch {
/// 失敗處理 ...
}
return request
}
複製代碼
func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>
方法回調後,能夠對數據進行驗籤解密。舉個 🌰 :
/// Called to modify a result before completion.
public func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError> {
/// 驗籤
if case .success(let response) = result {
do {
let responseString = try response.mapJSON()
/// Json 轉成 字典
let dic = JsonToDic(responseString)
/// 驗籤
if let _ = SignUntil.verifySign(withParamDic: dic) {
/// 數據解密
dic = RSA.decodeRSA(withParamDic: dic)
/// 從新生成 Moya.response
/// ...
/// 返回 Moya.response
return .success(response)
} else {
let error = NSError(domain: "驗籤失敗", code: 1, userInfo: nil)
return .failure(MoyaError.underlying(error, nil))
}
} catch {
let error = NSError(domain: "攔截器 response 轉 json 失敗", code: 1, userInfo: nil)
return .failure(MoyaError.underlying(error, nil))
}
} else {
/// 本來就失敗了就丟回了
return result
}
}
複製代碼
willSend
和 didReceive
作日誌打印:舉個 🌰 :
/// 準備發送的時候攔截打印日誌
public func willSend(_ request: RequestType, target: TargetType) {
/// 請求日誌打印
NetWorkingLoggerOutPut.outPutLoggerRequest(request.request, andRequestURL: request.request?.url?.absoluteString)
}
/// 將要接受的時候攔截打印日誌
public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
/// 返回日誌打印
switch result {
case .success(let response):
NetWorkingLoggerOutPut.outPutLoggerReponseString(response.response, andRequest: response.request, andResponseObj:tryResponseToJSON(response: response) )
case .failure(let error):
NetWorkingLoggerOutPut.outPutLoggerReponseString(error.response?.response, andRequest: error.response?.request, andResponseObj: tryResponseToJSON(response: error.response))
}
}
複製代碼
固然,這只是一些代碼片斷,可是重要代碼已經貼出來了,你能夠以此爲靈感繼續擴展。
一個請求在 init
的時候將 trackInflights
設置爲 true
,那麼在 Moya
中就會存儲這個請求的 endpoint
。在返回數據的時候,若是須要跟蹤了重複請求,那麼就將一次實際發送請求返回的數據,屢次返回。
3.1 和 3.2 基本上對 Moya 的使用詳細說明了,這裏就說調用方式吧。
let provider = MoyaProvider(endpointClosure: endpointClosure,
requestClosure: requestClosure,
stubClosure: stubClosure,
manager: manager,
plugins: plugins)
provider.request(.createUser("三","張")) { result in
do {
let response = try result.get()
let value = try response.mapNSArray()
self.repos = value
} catch {
let printableError = error as CustomStringConvertible
self.showAlert("GitHub Fetch", message: printableError.description)
}
}
複製代碼
若是使用 RxSwift
須要導入庫 RxMoya
,根據 Moya
官方主頁導入便可。
provider.rx.request(.createUser("三","張"))
.asObservable()
.mapJSON()
.mapHandyModel(type: UserModel.self)
.asSingle()
.subscribe { (userModel) in
} onFailure: { (error) in
} onDisposed: {
}
.disposable(by:disposable)
複製代碼
看完上面的內容,應該對 Moya
有必定的瞭解了,實際開發中,咱們須要涉及的東西至關的多。好比,不一樣的接口可能須要不一樣的網絡超時時間、還能可能須要配置接口需不須要對用戶信息的驗證,是否走本地測試數據,等等。
還有一些,好比 baseURL
,網絡請求頭 headers
, HTTPMethod
大多都是同樣的,若是每次都從新設置,那有一天改了 baseURL
的地址,headers
都須要增長一個參數,那時候殺人的心都有了。
既然 Moya
已經提供了 TargetType
咱們何不擴展一下呢?
public protocol BaseHttpAPIManager: TargetType {
///是否驗證用戶身份
var validUser : Bool { get }
///超時時間
var timeoutInterval : Double { get }
/// 是否走測試數據 默認 .never
var stubBehavior: Moya.StubBehavior { get }
/// 等等 ...
}
複製代碼
協議繼承完成以後,這裏就能夠對咱們基本不變化的參數進行賦值。
extension BaseHttpAPIManager {
public var baseURL: URL {
return URL(string: WebService.shared.BaseURL)!
}
public var method: Moya.Method {
return .post
}
public var sampleData: Data {
return "response: test data".data(using: String.Encoding.utf8)!
}
public var task: Task {
return .requestPlain
}
///是否驗證成功碼
public var validationType: Moya.ValidationType {
return .successCodes
}
///請求頭
public var headers: [String : String]? {
return WebService.shared.HttpHeaders
}
///如下爲自定義擴展
public var validUser : Bool {
return WebService.shared.ValidUser
}
public var timeoutInterval : Double {
return WebService.shared.TimeoutInterval
}
/// 是否走測試數據 默認 .never
public var stubBehavior: StubBehavior {
return .never
}
//...
}
複製代碼
由於 TargetType
協議是貫穿 Moya
整個核心的,因此你基本能夠在任意地方使用它。以後只須要實現遵照 BaseHttpAPIManager
協議就能夠了。
這裏我就不寫代碼了,我推薦一個 GitHub 上的 Demo 看一下,本菜雞也是從這裏借鑑的。
由於 HandyJson 能夠支持結構體。Swift
中若是不須要繼承的類,建議使用結構體,佔用內存更小。
聲明一個 struct
或者 class
,必須支持 HandyJSON
協議。
struct UserModel : HandyJSON {
var name : String?
var age : Int?
var address : String?
var hobby : [HobbyModel]? /// 支持模型數組,可是須要將數組中類型寫清楚
}
複製代碼
/// 普通模型轉換
let parsedElement = UserModel.deserialize(from: AnyObject)
/// 數組模型轉換
let parsedArray = [UserModel].deserialize(from: AnyObject)
複製代碼
擴展 Observable
就能夠了。
public extension Observable where Element : Any {
/// 普通 Json 轉 Model
func mapHandyModel <T : HandyJSON> (type : T.Type) -> Observable<T?> {
return self.map { (element) -> T? in
/// 這裏的data 是 String 或者 dic
let data = element
let parsedElement : T?
if let string = data as? String {
parsedElement = T.deserialize(from: string)
} else if let dictionary = data as? Dictionary<String , Any> {
parsedElement = T.deserialize(from: dictionary)
} else if let dictionary = data as? [String : Any] {
parsedElement = T.deserialize(from: dictionary)
} else {
parsedElement = nil
}
return parsedElement
}
}
// 將 Json 轉成 模型數組
func mapHandyModelArray<T: HandyJSON>(type: T.Type) -> Observable<[T?]?> {
return self.map { (element) -> [T?]? in
/// 這裏的data 是 String 或者 dic
let data = element
let parsedArray : [T?]?
if let string = data as? String {
parsedArray = [T].deserialize(from: string)
} else if let array = data as? [Any] {
parsedArray = [T].deserialize(from: array)
} else {
parsedArray = nil
}
return parsedArray
}
}
}
複製代碼
聯合方式上方 3.3.2 Moya RxSwift 調用方式
已經給出了。
json.rx.mapHandyModel(type: UserModel.self)
.asSingle()
.subscribe { (userModel) in
} onFailure: { (error) in
} onDisposed: {
}
.disposable(by:disposable)
複製代碼
關於 RxSwift
的使用方式看 Cooci 的博客 RxSwift 用法。
有了這些,你就能夠快速搭建新項目的網絡請求了,若是感受幫助了你些許,能給個贊最好了,感謝各位。