#序文
前回の記事のソースコードでNetworkServiceを見、 +の使用はまだ非常に原始的であることがわかります。それでは、以下をカプセル化してみましょう。Moya
RxSwift
NetworkService
-
ネットワークリクエストの結果をキャッシュし、起動時に最初にローカルキャッシュデータを表示します
-
毎回要求する必要のないデータに時間ベースのキャッシュを提供する
-
外界への統一された
RxSwift
インターフェース。新しい関数の場合、対応する関数の呼び出しに注釈を付けるだけでよく、後続のメソッドを変更する必要はありません。
1.統合ネットワーク要求のインターフェース
この記事では、グローバル変数を使用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()
复制代码
これで最初のステップは終了です。
2.時間ごとにキャッシュ機能を増やします
最初にキャッシュ時間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
ブランチ。