概述
几乎所有的项目都需要网络请求,因为他可以给用户呈现更加丰富的内容,方便我们在不同设备之间管理同步数据。网络请求会出现在你项目的各个地方:启动页,列表页,登录注册...所以如何管理组织网络请求是 App 架构中非常重要的一部分。Github 上也有类似的框架比如 Moya
, 我们且称其为网络框架的框架吧。Moya
也有这个框架也发展了很久了,功能很强大,社区也一直很活跃,也有衍生的 RxMoya
和 ReactiveMoya
。但我在使用过后发现他过于 重
了,一直觉得他那种 path
+ method
+ parameter
拆开的写法太过于繁琐,那么本文就让我们来一步步搭建适合自己的网络框架吧。
提示:为了方便和通用,我们的网络请求 API 就直接基于 Alamofire 来写好了。
分析和思路
首先我们来看看最简单的请求长什么样?
AF.request("https://httpbin.org/get").response { response in
debugPrint(response)
}
非常简单对不对?但现实并不是这样,在一个请求中我们需要处理各种疑难杂症,最终一个请求的代码可能会很长很长(长到一个屏幕都放不下!),所以我们要尽量抽象和复用这里的逻辑。
如何下手呢?解决一个问题的最常用的方法就是先看清楚问题,然后把大问题拆成小问题,再一个个解决。我们先来思考下🤔,一个完整请求要做的事情什么:
- 将 url,method,body 封装成一个
HTTP Request
对象, - 设置请求的
HTTP Header
- 接受
HTTP Request
回来的 data 数据 - 处理
error
和response code
- 通过
codable
之类的框架将raw data
转换model
对象 - 请求重试
在常规的业务中,2、3、4、5 往往是可以统一抽象处理的,而最常见做法就是用一个 HTTPClient
或 APIManager
来统一 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)
}
}
}
}
总结
对我来说要做好一份适用性很强的网络架构不是一件很容易的事情,实际上网络请求的复杂度远远不止于此,这里做的仅仅是把一些通用逻辑统一处理,还有很多本文没有讲到。比如
- 如何用单一功能原则(Single responsibility principle)优化这里的逻辑
- 当我们遇到服务端返回的不标准的数据结构怎么处理?
- 使用
Codable
区分返回的是 Array 还是 Object 是要不同处理的 - 当我们有多个 API 地址比如测试环境和正式环境,那么我们如何去管理?
这些都是我们需要去解决的。我当初也踩了无数坑,太难了。 这些问题先留给大家思考吧😆