如何設計你的網絡請求

概述

幾乎全部的項目都須要網絡請求,由於他能夠給用戶呈現更加豐富的內容,方便咱們在不一樣設備之間管理同步數據。網絡請求會出如今你項目的各個地方:啓動頁,列表頁,登陸註冊...因此如何管理組織網絡請求是 App 架構中很是重要的一部分。Github 上也有相似的框架好比 Moya, 咱們且稱其爲網絡框架的框架吧。Moya 也有這個框架也發展了好久了,功能很強大,社區也一直很活躍,也有衍生的 RxMoyaReactiveMoya 。但我在使用事後發現他過於 了,一直以爲他那種 path + method + parameter 拆開的寫法太過於繁瑣,那麼本文就讓咱們來一步步搭建適合本身的網絡框架吧。git

提示:爲了方便和通用,咱們的網絡請求 API 就直接基於 Alamofire 來寫好了。github

分析和思路

首先咱們來看看最簡單的請求長什麼樣?編程

AF.request("https://httpbin.org/get").response { response in
    debugPrint(response)
}
複製代碼

很是簡單對不對?但現實並非這樣,在一個請求中咱們須要處理各類疑難雜症,最終一個請求的代碼可能會很長很長(長到一個屏幕都放不下!),因此咱們要儘可能抽象和複用這裏的邏輯。json

如何下手呢?解決一個問題的最經常使用的方法就是先看清楚問題,而後把大問題拆成小問題,再一個個解決。咱們先來思考下🤔,一個完整請求要作的事情什麼:swift

  1. 將 url,method,body 封裝成一個 HTTP Request 對象,
  2. 設置請求的 HTTP Header
  3. 接受 HTTP Request 回來的 data 數據
  4. 處理 errorresponse code
  5. 經過 codable 之類的框架將 raw data 轉換 model 對象
  6. 請求重試

在常規的業務中,二、三、四、5 每每是能夠統一抽象處理的,而最多見作法就是用一個 HTTPClientAPIManager 來統一 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 真香系列😋

不會 RxSwift 建議你們都去學一啦,響應式編程真的很棒棒數據結構

如何支持 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)
      }
    }
  }
}

複製代碼

總結

對我來講要作好一份適用性很強的網絡架構不是一件很容易的事情,實際上網絡請求的複雜度遠遠不止於此,這裏作的僅僅是把一些通用邏輯統一處理,還有不少本文沒有講到。好比

  1. 如何用單一功能原則(Single responsibility principle)優化這裏的邏輯
  2. 當咱們遇到服務端返回的不標準的數據結構怎麼處理?
  3. 使用 Codable 區分返回的是 Array 仍是 Object 是要不一樣處理的
  4. 當咱們有多個 API 地址好比測試環境和正式環境,那麼咱們如何去管理?

這些都是咱們須要去解決的。我當初也踩了無數坑,太難了。 這些問題先留給你們思考吧😆

參考

最後仍是大力推薦下喵神在臺灣 iPlayground 的演講

www.youtube.com/watch?v=Xk4…

相關文章
相關標籤/搜索