對沸點頁面仿寫的補充-網路層補充
# 前言
如果您已經看過 上篇 原始碼中的 NetworkService ,您會發現對於 Moya
+ RxSwift
的使用還是十分的原始。現在讓我們嘗試封裝以下 NetworkService
,提供 :
-
快取網路請求結果,啟動時先顯示本地快取資料
-
對於不需要每次都請求的資料提供按時間快取功能
-
對外提供統一的
RxSwift
介面,對於新功能只需要註釋對應功能的呼叫,不需要修改後續方法
一、 統一網路請求的介面
在篇文章中我們使用了,全域性變數 kDynamicProvider
來進行網路請求:
// 宣告為全域性變數
let kDynamicProvider = MoyaProvider<XTNetworkService>()
...
...
// 網路請求
kDynamicProvider.rx.request(.list(param: param.toJsonDict()))
對於不同的介面(如:文章相關介面)每個都需要重複提供這種全域性變數的形式,這不利於統一新增 Plugins
等。而全部的介面都使用同一個 MoyaProvider
例項又會增加 enum
中的程式碼量不利於程式碼閱讀和維護。因此,這一部分是我們首先要封裝的。
首先建立 XTNetworkCacheExtension.swift
檔案新增如下程式碼:
```Swift import Foundation import RxSwift import Moya
/// 實際傳送網路請求的 provider
private let xtProvider = MoyaProvider
public extension TargetType {
/// 直接進行網路請求
func request() -> Single<Response> {
return xtProvider.rx.request(.target(self))
}
} ```
現在可以刪除 kDynamicProvider
然後回到 DynamicListViewModel 中如下替換掉 kDynamicProvider
```Swift // 需要替換的程式碼 kDynamicProvider.rx.request(.list(param: param.toJsonDict()))
// 最終程式碼 DynamicNetworkService.list(param: param.toJsonDict()).request() ```
至此第一步結束。
二、增加按時間快取功能
先把快取時間 cacheTime
和 TargetType
定義為一個 元祖
Swift
public typealias CacheTimeTargetTuple = (cacheTime: TimeInterval, target: TargetType)
在 extension TargetType
中的 request
方法後新增按時間快取的介面:
Swift
/// 使用時間快取策略, 記憶體中有資料就不請求網路
func memoryCacheIn(_ seconds: TimeInterval = 180) -> Single<CacheTimeTargetTuple> {
return Single.just((seconds, self))
}
備註:這裡要補充一個知識點--如果您閱讀過 RxSwift
的原始碼您應該已經知道的知識點:
Swift
public typealias Single<Element> = PrimitiveSequence<SingleTrait, Element>
Single
是 PrimitiveSequence<SingleTrait>
的別名,因此為了提供 request
介面我們需要對 PrimitiveSequence<SingleTrait, CacheTimeTargetTuple>
進行拓展,程式碼如下:
```Swift 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:)
```Swift 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
中增加讀取快取的程式碼:
```Swift /// 記憶體中快取的資料 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
}
} ```
此功能完成,最終我沒可以如下呼叫快取介面:
Swift
DynamicNetworkService.topicListRecommend
.memoryCacheIn()
.request()
不使用快取時只需要註釋掉 .memoryCacheIn()
,即可。
# 實現 disk 快取功能
對於 disk
快取,這裡提供另外一種封裝方式,使用 struct OnDiskStorage<Target: TargetType, T: Codable>
來實現相關功能。
1) 宣告 OnDiskStorage
:
```Swift // MARK: - 在磁碟中的快取
public struct OnDiskStorage
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)
}
}
} ```
2) 對 TargetType
新增 onStorage
, writeToDiskStorage
和 readDiskStorage
方法
```Swift
/// 讀取磁碟快取, 一般用於啟動時先載入資料, 而後真正的讀取網路資料
func onStorage
return OnDiskStorage(target: self, keyPath: keyPath)
}
/// 從磁碟讀取
fileprivate func readDiskStorage
fileprivate func writeToDiskStorage
功能完成,現在您可以如下使用介面:
Swift
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 拓展
```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
{ 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
拓展快取相關程式碼```Swift // 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
分支。