对沸点页面仿写的补充-网络层补充

# 前言

如果您现已看过 上篇 源码中的 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()

至此第一步完毕。

二、增加按时刻缓存功用

先把缓存时刻 cacheTimeTargetType 定义为一个 元祖

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>

SinglePrimitiveSequence<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 中只要是对 PrimitiveSequenceextension 才能直接运用 flatMap (此处省掉 return)
  • 2 中咱们运用了cache进行 memorydisk 存储
  • 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> 来完结相关功用。

  1. 声明 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)
        }
    }
}
  1. TargetType 增加 onStorage, writeToDiskStoragereadDiskStorage 办法
/// 读取磁盘缓存, 一般用于启动时先加载数据, 而后真正的读取网络数据
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 分支。