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

语言: 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 分支。