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

2,239 阅读4分钟

0.JPEG

# 前言

如果您已经看过 上篇 源码中的 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 分支。