# 前言
如果您已经看过 上篇 源码中的 NetworkService ,您会发现对于 Moya
+ RxSwift
的使用还是十分的原始。现在让我们尝试封装以下 NetworkService
,提供 :
-
缓存网络请求结果,启动时先显示本地缓存数据
-
对于不需要每次都请求的数据提供按时间缓存功能
-
对外提供统一的
RxSwift
接口,对于新功能只需要注释对应功能的调用,不需要修改后续方法
一、 统一网络请求的接口
在篇文章中我们使用了,全局变量 kDynamicProvider
来进行网络请求:
// 声明为全局变量
let kDynamicProvider = MoyaProvider<XTNetworkService>()
...
...
// 网络请求
kDynamicProvider.rx.request(.list(param: param.toJsonDict()))
对于不同的接口(如:文章相关接口)每个都需要重复提供这种全局变量的形式,这不利于统一添加 Plugins
等。而全部的接口都使用同一个 MoyaProvider
实例又会增加 enum
中的代码量不利于代码阅读和维护。因此,这一部分是我们首先要封装的。
首先创建 XTNetworkCacheExtension.swift
文件添加如下代码:
import Foundation
import RxSwift
import Moya
/// 实际发送网络请求的 provider
private let xtProvider = MoyaProvider<MultiTarget>()
public extension TargetType {
/// 直接进行网络请求
func request() -> Single<Response> {
return xtProvider.rx.request(.target(self))
}
}
现在可以删除 kDynamicProvider
然后回到 DynamicListViewModel 中如下替换掉 kDynamicProvider
// 需要替换的代码
kDynamicProvider.rx.request(.list(param: param.toJsonDict()))
// 最终代码
DynamicNetworkService.list(param: param.toJsonDict()).request()
至此第一步结束。
二、增加按时间缓存功能
先把缓存时间 cacheTime
和 TargetType
定义为一个 元祖
public typealias CacheTimeTargetTuple = (cacheTime: TimeInterval, target: TargetType)
在 extension TargetType
中的 request
方法后添加按时间缓存的接口:
/// 使用时间缓存策略, 内存中有数据就不请求网络
func memoryCacheIn(_ seconds: TimeInterval = 180) -> Single<CacheTimeTargetTuple> {
return Single.just((seconds, self))
}
备注:这里要补充一个知识点--如果您阅读过 RxSwift
的源码您应该已经知道的知识点:
public typealias Single<Element> = PrimitiveSequence<SingleTrait, Element>
Single
是 PrimitiveSequence<SingleTrait>
的别名,因此为了提供 request
接口我们需要对 PrimitiveSequence<SingleTrait, CacheTimeTargetTuple>
进行拓展,代码如下:
extension PrimitiveSequence where Trait == SingleTrait, Element == CacheTimeTargetTuple {
public func request() -> Single<Response> {
// 1.
flatMap { tuple -> Single<Response> in
let target = tuple.target
// 2.
if let response = target.cachedResponse() {
return .just(response)
}
// 3.
let cacheKey = target.cacheKey
let seconds = tuple.cacheTime
// 4.
let result = target.request().cachedIn(seconds: seconds, cacheKey: cacheKey)
return result
}
}
}
- 在
1
中只有是对PrimitiveSequence
的extension
才能直接使用flatMap
(此处省略return
) - 在
2
中我们使用了cache进行memory
和disk
存储 - 在
3
中是我们拓展的缓存key
,具体代码见文末补充,或参阅github
源码 - 在
4
中的cachedIn(seconds:, cacheKey:)
就是我们实际进行memory
缓存的代码
实现 func cachedIn(seconds:, cacheKey:)
extension PrimitiveSequence where Trait == SingleTrait, Element == Response {
fileprivate func cachedIn(seconds: TimeInterval, cacheKey: String) -> Single<Response> {
flatMap { response -> Single<Response> in
kMemoryStroage.setObject(response, forKey: cacheKey, expiry: .seconds(seconds))
return .just(response)
}
}
}
在 TargetType
中增加读取缓存的代码:
/// 内存中缓存的数据
fileprivate func cachedResponse() -> Response? {
do {
let cacheData = try kMemoryStroage.object(forKey: cacheKey)
if let response = cacheData as? Response {
return response
} else {
return nil
}
} catch {
print(error)
return nil
}
}
此功能完成,最终我没可以如下调用缓存接口:
DynamicNetworkService.topicListRecommend
.memoryCacheIn()
.request()
不使用缓存时只需要注释掉 .memoryCacheIn()
,即可。
# 实现 disk 缓存功能
对于 disk
缓存,这里提供另外一种封装方式,使用 struct OnDiskStorage<Target: TargetType, T: Codable>
来实现相关功能。
- 声明
OnDiskStorage
:
// MARK: - 在磁盘中的缓存
public struct OnDiskStorage<Target: TargetType, T: Codable> {
fileprivate let target: Target
private var keyPath: String = ""
fileprivate init(target: Target, keyPath: String) {
self.target = target
self.keyPath = keyPath
}
/// 每个包裹的结构体都提供 request 方法, 方便后续链式调用时去除不想要的功能
///
/// 如 `provider.memoryCacheIn(3*50).request()` 中去除 `.memoryCacheIn(3*50)` 仍能正常使用
public func request() -> Single<Response> {
return target.request().flatMap { response -> Single<Response> in
do {
let model = try response.map(T.self)
try target.writeToDiskStorage(model)
} catch {
// nothings to do
print(error)
}
return .just(response)
}
}
}
- 对
TargetType
添加onStorage
,writeToDiskStorage
和readDiskStorage
方法
/// 读取磁盘缓存, 一般用于启动时先加载数据, 而后真正的读取网络数据
func onStorage<T: Codable>(_ type: T.Type, atKeyPath keyPath: String = "", onDisk: ((T) -> ())?) -> OnDiskStorage<Self, T> {
if let storage = readDiskStorage(type) { onDisk?(storage) }
return OnDiskStorage(target: self, keyPath: keyPath)
}
/// 从磁盘读取
fileprivate func readDiskStorage<T: Codable>(_ type: T.Type) -> T? {
do {
let config = DiskConfig(name: "\(type.self)")
let transformer = TransformerFactory.forCodable(ofType: type.self)
let storage = try DiskStorage<String, T>.init(config: config, transformer: transformer)
let model = try storage.object(forKey: cacheKey)
return model
} catch {
print(error)
return nil
}
}
fileprivate func writeToDiskStorage<T: Codable>(_ model: T) throws {
let config = DiskConfig(name: "\(T.self)")
let transformer = TransformerFactory.forCodable(ofType: T.self)
let storage = try DiskStorage<String, T>.init(config: config, transformer: transformer)
try storage.setObject(model, forKey: cacheKey)
}
功能完成,现在您可以如下使用接口:
DynamicNetworkService.list(param: param.toJsonDict()).onStorage(XTListResultModel.self) { [weak self] diskModel in
// 使用 disk model 填充 UI
self?.diskCacheSubject.onNext(diskModel)
}.request()
至此,对 Moya
的简单封装已经完成,感谢您的阅读
补充:
# 缓存 key 相关代码
于缓存的 key
这里有两种做法,一个是从 TargetType
实例生成,一个是外部传入,这里使用 TargetType
生成缓存 key
,具体代码如下:
-
对 Swift 拓展
// MARK: - Swift.Collection private extension String { var sha256: String { guard let data = data(using: .utf8) else { return self } var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) _ = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in return CC_SHA256(bytes.baseAddress, CC_LONG(data.count), &digest) } return digest.map { String(format: "%02x", $0) }.joined() } } // TODO: - 需要做测试 XCTest private extension Optional { var stringValue: String { switch self { case .none: return "" case .some(let wrapped): return "\(wrapped)" } } } private extension Optional where Wrapped == Dictionary<String, Any> { var stringValue: String { switch self { case .none: return "" case .some(let wrapped): let allKeys = wrapped.keys.sorted() return allKeys.map { $0 + ":" + wrapped[$0].stringValue }.joined(separator: ",") } } } private extension Optional where Wrapped: Collection, Wrapped.Element: Comparable { var stringValue: String { switch self { case .none: return "" case .some(let wrapped): return wrapped.sorted().reduce("") { $0 + "\($1)" } } } } private extension Dictionary where Key == String { var sortedDescription: String { let allKeys = self.keys.sorted() return allKeys.map { $0 + ":" + self[$0].stringValue }.joined(separator: ",") } }
-
对
TargetType
拓展缓存相关代码// MARK: - 缓存相关 fileprivate extension TargetType { /// 缓存的 key var cacheKey: String { let key = "\(method)\(URL(target: self).absoluteString)\(self.path)?\(task.parameters)" return key.sha256 } } fileprivate extension Task { var canCactch: Bool { switch self { case .requestPlain: fallthrough case .requestParameters(_, _): fallthrough case .requestCompositeData(_, _): fallthrough case .requestCompositeParameters(_ , _, _): return true default: return false } } var parameters: String { switch self { case .requestParameters(let parameters, _): return parameters.sortedDescription case .requestCompositeData(_, let urlParameters): return urlParameters.sortedDescription case .requestCompositeParameters(let bodyParameters, _, let urlParameters): return bodyParameters.sortedDescription + urlParameters.sortedDescription default: return "" } } }
# 源码
XTDemo SUN
分支。