如何设计你的网络请求

3,153 阅读5分钟

概述

几乎所有的项目都需要网络请求,因为他可以给用户呈现更加丰富的内容,方便我们在不同设备之间管理同步数据。网络请求会出现在你项目的各个地方:启动页,列表页,登录注册...所以如何管理组织网络请求是 App 架构中非常重要的一部分。Github 上也有类似的框架比如 Moya, 我们且称其为网络框架的框架吧。Moya 也有这个框架也发展了很久了,功能很强大,社区也一直很活跃,也有衍生的 RxMoyaReactiveMoya 。但我在使用过后发现他过于 了,一直觉得他那种 path + method + parameter 拆开的写法太过于繁琐,那么本文就让我们来一步步搭建适合自己的网络框架吧。

提示:为了方便和通用,我们的网络请求 API 就直接基于 Alamofire 来写好了。

分析和思路

首先我们来看看最简单的请求长什么样?

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

非常简单对不对?但现实并不是这样,在一个请求中我们需要处理各种疑难杂症,最终一个请求的代码可能会很长很长(长到一个屏幕都放不下!),所以我们要尽量抽象和复用这里的逻辑。

如何下手呢?解决一个问题的最常用的方法就是先看清楚问题,然后把大问题拆成小问题,再一个个解决。我们先来思考下🤔,一个完整请求要做的事情什么:

  1. 将 url,method,body 封装成一个 HTTP Request 对象,
  2. 设置请求的 HTTP Header
  3. 接受 HTTP Request 回来的 data 数据
  4. 处理 errorresponse code
  5. 通过 codable 之类的框架将 raw data 转换 model 对象
  6. 请求重试

在常规的业务中,2、3、4、5 往往是可以统一抽象处理的,而最常见做法就是用一个 HTTPClientAPIManager 来统一 handle 这类逻辑了。而对于 1 每个请求的参数、地址、方法都不一样所以我们还是会将他们暴露出去,最终大概长这样:

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)
    }
  }
}

最后我们发起请求的方法大概长这样:

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…