對沸點頁面仿寫的補充-網路層補充

語言: CN / TW / HK

0.JPEG

# 前言

如果您已經看過 上篇 原始碼中的 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() ```

至此第一步結束。

二、增加按時間快取功能

先把快取時間 cacheTimeTargetType 定義為一個 元祖

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>

SinglePrimitiveSequence<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 中只有是對 PrimitiveSequenceextension 才能直接使用 flatMap (此處省略 return)
  • 2 中我們使用了cache進行 memorydisk 儲存
  • 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 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)
    }
}

} ```

2) 對 TargetType 新增 onStorage, writeToDiskStoragereadDiskStorage 方法

```Swift /// 讀取磁碟快取, 一般用於啟動時先載入資料, 而後真正的讀取網路資料 func onStorage(_ type: T.Type, atKeyPath keyPath: String = "", onDisk: ((T) -> ())?) -> OnDiskStorage { if let storage = readDiskStorage(type) { onDisk?(storage) }

return OnDiskStorage(target: self, keyPath: keyPath)

}

/// 從磁碟讀取 fileprivate func readDiskStorage(_ type: T.Type) -> T? { do { let config = DiskConfig(name: "(type.self)") let transformer = TransformerFactory.forCodable(ofType: type.self) let storage = try DiskStorage.init(config: config, transformer: transformer) let model = try storage.object(forKey: cacheKey) return model } catch { print(error) return nil } }

fileprivate func writeToDiskStorage(_ model: T) throws { let config = DiskConfig(name: "(T.self)") let transformer = TransformerFactory.forCodable(ofType: T.self) let storage = try DiskStorage.init(config: config, transformer: transformer) try storage.setObject(model, forKey: cacheKey) } ```

功能完成,現在您可以如下使用介面:

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 分支。