幾乎全部的項目都須要網絡請求,由於他能夠給用戶呈現更加豐富的內容,方便咱們在不一樣設備之間管理同步數據。網絡請求會出如今你項目的各個地方:啓動頁,列表頁,登陸註冊...因此如何管理組織網絡請求是 App 架構中很是重要的一部分。Github 上也有相似的框架好比 Moya
, 咱們且稱其爲網絡框架的框架吧。Moya
也有這個框架也發展了好久了,功能很強大,社區也一直很活躍,也有衍生的 RxMoya
和 ReactiveMoya
。但我在使用事後發現他過於 重
了,一直以爲他那種 path
+ method
+ parameter
拆開的寫法太過於繁瑣,那麼本文就讓咱們來一步步搭建適合本身的網絡框架吧。git
提示:爲了方便和通用,咱們的網絡請求 API 就直接基於 Alamofire 來寫好了。github
首先咱們來看看最簡單的請求長什麼樣?編程
AF.request("https://httpbin.org/get").response { response in
debugPrint(response)
}
複製代碼
很是簡單對不對?但現實並非這樣,在一個請求中咱們須要處理各類疑難雜症,最終一個請求的代碼可能會很長很長(長到一個屏幕都放不下!),因此咱們要儘可能抽象和複用這裏的邏輯。json
如何下手呢?解決一個問題的最經常使用的方法就是先看清楚問題,而後把大問題拆成小問題,再一個個解決。咱們先來思考下🤔,一個完整請求要作的事情什麼:swift
HTTP Request
對象,HTTP Header
HTTP Request
回來的 data 數據error
和 response code
codable
之類的框架將 raw data
轉換 model
對象在常規的業務中,二、三、四、5 每每是能夠統一抽象處理的,而最多見作法就是用一個 HTTPClient
或 APIManager
來統一 handle 這類邏輯了。而對於 1
每一個請求的參數、地址、方法都不同因此咱們仍是會將他們暴露出去,最終大概長這樣:api
warning:下面方法只是替提供思路,部分代碼被省略網絡
class HTTPClient {
var host: String
init(host: String) {
self.host = host
}
// 設置 timeout
private let sessionManager: SessionManager = {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 15
let sessionManager = SessionManager(configuration: config)
return sessionManager
}()
// 設置 HTTPHeaders
private var defaultHeader: HTTPHeaders = {
var defaultHTTPHeaders = SessionManager.defaultHTTPHeaders
defaultHTTPHeaders["User-Agent"] = "You device user agent"
defaultHTTPHeaders["Accept-Encoding"] = acceptEncoding
// 在 header 中添加 token
return defaultHTTPHeaders
}()
}
extension HTTPClient {
@discardableResult
func requestObject<T: Codable>(path: String, method: Alamofire.HTTPMethod = .get, parameters:[String:Any?]?, handler: @escaping (T?, Error?) -> Void) -> Request {
// json -> model
return buidRequest(path: path, method: method, parameters: parameters) { [weak self](dataSource, error) in
if let error = error {
handler(nil, error)
return
}
// 經過 `codable` 框架將 raw data 轉換 model 對象
do {
let model = try dataSource.data?.mapObject(Response<T>.self).data
handler(model, nil)
} catch let error {
let parseError = HTTPClientError(code:.decodeError,localDescrition:"parse_error".localized)
self?.showDecodingError(path: path, error: error)
handler(nil, parseError)
}
}
}
}
// MARK: - Private Methods
private extension HTTPClient {
/// note: data used by codable
typealias CompletionHandler = ((data: Data?, result: Any?), Error?) -> Void
@discardableResult
private func buidRequest(path:String, method: Alamofire.HTTPMethod, parameters:[String:Any?]?, handler: @escaping CompletionHandler) -> Request {
// filter nil value
let validParas = parameters?.compactMapValues { $0 }
let request = sessionManager.request(host + path, method: method, parameters: validParas, headers: defaultHeader)
return request.responseJSON { response in
// 4. 處理 error 和 response code
self.handelResponse(response: response, handler: handler)
}
}
}
複製代碼
最後咱們發起請求的方法大概長這樣:session
static func auth(from: String, token: String) -> AuthResult? {
let path = "wp-json/wc/v3/third/party/access/token"
let parameters = ["from": from, "third_access_token": token]
return HTTPClient.shared.requestObject(path: path, parameters: parameters)
}
複製代碼
不會 RxSwift
建議你們都去學一啦,響應式編程真的很棒棒數據結構
extension HTTPClient: ReactiveCompatible {}
extension Reactive where Base: HTTPClient {
/// Designated request-making method.
///
/// - Parameters:
/// - path: url path
/// - parameters: A dictionary of parameters to apply to a `URLRequest`
/// - Returns: Response of singleobject.
func requestObject<T: Codable>(path:String, method: HTTPMethod = .get, parameters:[String:Any?]?) -> Single<T?> {
return Single.create { single in
let request = self.base.requestObject(path: path, method: method, parameters: parameters, handler: { (model: T?, error) in
if let error = error {
single(.error(error))
} else {
single(.success(model))
}
})
return Disposables.create {
request.cancel()
}
}
}
}
複製代碼
得益與 RxSwift 重試和請求合併不是常簡單。架構
// 請求合併
Observable.zip(request1, request2, request3)
.subscribe(onNext: { (resp1, resp2, resp3) in
})
.disposed(by: disposeBag)
// 請求重試
HTTPClient.rx.user()
.asObservable()
.catchErrorJustReturn(nil)
.retry(3, delay: .constant(time: 3))
.disposed(by: disposeBag)
// RxSwift+Retry
enum DelayOptions {
case immediate
case constant(time: Double)
case exponential(initial: Double, multiplier: Double, maxDelay: Double)
case custom(closure: (Int) -> Double)
}
extension DelayOptions {
func make(_ attempt: Int) -> Double {
switch self {
case .immediate: return 0.0
case .constant(let time): return time
case .exponential(let initial, let multiplier, let maxDelay):
// if it's first attempt, simply use initial delay, otherwise calculate delay
let delay = attempt == 1 ? initial : initial * pow(multiplier, Double(attempt - 1))
return min(maxDelay, delay)
case .custom(let closure): return closure(attempt)
}
}
}
/// 主要是用於網絡請求的重試,能夠設置重試次數,重試之間的間隔,以及有網絡開始重試的邏輯
/// reference:http://kean.github.io/post/smart-retry
extension ObservableType {
/// Retries the source observable sequence on error using a provided retry
/// strategy.
/// - parameter maxAttemptCount: Maximum number of times to repeat the
/// sequence. `Int.max` by default.
/// - parameter didBecomeReachable: Trigger which is fired when network
/// connection becomes reachable.
/// - parameter shouldRetry: Always returns `true` by default.
func retry(_ maxAttemptCount: Int = Int.max, delay: DelayOptions, didBecomeReachable: Observable<Void> = Reachability.shared.didBecomeReachable, shouldRetry: @escaping (Error) -> Bool = { _ in true }) -> Observable<Element> {
return retryWhen { (errors: Observable<Error>) in
return errors.enumerated().flatMap { attempt,error -> Observable<Void> in
guard shouldRetry(error),
maxAttemptCount > attempt + 1 else {
return .error(error)
}
let timer = Observable<Int>
.timer(RxTimeInterval.seconds(Int(delay.make(attempt + 1))),
scheduler: MainScheduler.instance)
.map { _ in () }
return Observable.merge(timer, didBecomeReachable)
}
}
}
}
複製代碼
對我來講要作好一份適用性很強的網絡架構不是一件很容易的事情,實際上網絡請求的複雜度遠遠不止於此,這裏作的僅僅是把一些通用邏輯統一處理,還有不少本文沒有講到。好比
Codable
區分返回的是 Array 仍是 Object 是要不一樣處理的這些都是咱們須要去解決的。我當初也踩了無數坑,太難了。 這些問題先留給你們思考吧😆