编程 3

构建Moya便捷解析库,数据缓存

By admin in 编程 on 2019年9月12日

View Controller 向来是 MVC (Model-View-View Controller)
中最让人头疼的一环,MVC
架构本身并不复杂,但开发者很容易将大量代码扔到用于协调 View 和 Model
的 Controller 中。你不能说这是一种错误,因为 View Controller
所承担的本来就是胶水代码和业务逻辑的部分。但是,持续这样做必定将导致
Model View Controller 变成 Massive View
Controller,代码也就一天天烂下去,直到没人敢碰。

RxSwift+Moya+ObjectMapper优雅的网络请求本人已写了一个开源的拿来即用的Swift项目框架
可以参考此框架来通篇阅读此文章更有帮助 有喜欢的还望送人玫瑰手留余香哦
当然这个拿来即用的Swift项目框架有不合理的地方
还望大神指点一二

1、相信大家在使用Swift开发时,Moya是首选的网络工具,在模型解析这一块,Swift版模型解析的相关第三方库有很多,本人最习惯用的就是SwiftyJSON。2、下面会开始讲解整个主要的开发功能与思想。3、以下内容是基于大家会使用Moya和SwiftJSON的前提下所著,还不会的同学可以先简单了解后再来阅读本篇文章哦~

写到后来,几经变换,最后你的 Controller 常常就变成了这样

末尾有彩蛋

1、尝试模型解析

Moya请求服务器返回的数据以Response类返回给我们,那我们就给Response类做一个扩展,这里以解析模型为例

// 需要传入一个参数,告知我们要转换出什么模型public func mapObject<T: Modelable>(_ type: T.Type) -> T { // 模型解析过程 。。。 return T}

Q: 那中间的解析过程该怎么写呢?A:
可以让开发者遵守某个协议,实现指定的转换方法并描述转换关系。其转换过程我们不需要知道,交给开发者即可。

那接着我们来定义一个协议Modelable,并声明转换方法

public protocol Modelable { mutating func mapping(_ json: JSON)}

开发者创建一个MyMoel的结构体,遵守协议Modelable,并实现mapping,书写转换关系

struct MyModel: Modelable { var _id = "" mutating func mapping(_ json: JSON) { self._id = json["_id"].stringValue }}

以目前的现状来分析一下:mapObject可以让开发者传入模型类型,而我们的协议方法却并非是个类方法。那我们需要先得到这个模型类型的对象,再来调用mapping方法

编程 1

文章目录
一 Rxswift 简单介绍
二 Moya 简单介绍以及使用
三 ObjectMapper 简单介绍以及使用
四 RxSwift+Moya+ObjectMapper优雅的网络请求及模型转换

2、模型解析的驱动开发

Q: 怎么得到这个对象?A:
可以在协议中声明一个初始化方法来创建对象。是的,我们在mapObject中创建对应模型类型的对象,调用mapping方法来转换数据,再把模型对象传出去即可。

那我们在Modelable中声明一个init方法,并传入一个参数,区别于其它初始化方法

public protocol Modelable { mutating func mapping(_ json: JSON) init(_ json: JSON)}

OK,现在把mapObject方法补齐模型解析过程

public func mapObject<T: Modelable>(_ type: T.Type) -> T { let modelJson = JSON["modelKey"] // 模型解析过程 var obj = T.init(modelJson) obj.mapping(modelJson) return obj}

Controller 中含有大量代码的一个很大原因在于,大多数人都误用了
MVC,推荐可以看看喵神的这两篇文章,深入浅出。关于 MVC
的一个常见的误用单向数据流动的函数式 View Controller

一 RxSwift 简单介绍

  • RxSwift是Swift函数响应式编程的一个开源库,由GitHub的ReactiveX组织开发和维护
  • 其他语言像C#,Java 和JS也有: Rx.Net、RxJava、RxJS
  • RxSwift的目的是让数据/事件流和异步任务能够更方便的序
    列化处理 能够使用Swift进行响应式编程
    本文就不详细介绍Rxswift 楼主之前写过RxSwift 的介绍以及使用
    可以参看一下 RxSwift
    个人学习笔记记录

3、自定义解析键名

Q:
这样是搞定解析了,但是网络请求回来的json格式错综复杂,有什么办法可以让开发者来自行指定model对应的键名呢?A:
嗯嗯,既然解析过程是在 Response 扩展里操作的,那我们可以通过协议定义键名属性,并且使用 Runtime 给Response动态添加一个属性,来记录遵守协议后的相应类名

public protocol ModelableParameterType { /// 请求成功时状态码对应的值 static var successValue: String { get } /// 状态码对应的键 static var statusCodeKey: String { get } /// 请求后的提示语对应的键 static var tipStrKey: String { get } /// 请求后的主要模型数据的键 static var modelKey: String { get }}

// MARK:- runtimeextension Response { private struct AssociatedKeys { static var lxf_modelableParameterKey = "lxf_modelableParameterKey" } var lxf_modelableParameter: ModelableParameterType.Type { get { let value = objc_getAssociatedObject(self, &AssociatedKeys.lxf_modelableParameterKey) as AnyObject guard let type = value as? ModelableParameterType.Type else { return NullParameter.self } return type } set { objc_setAssociatedObject(self, &AssociatedKeys.lxf_modelableParameterKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } }}

这里有个坑:_SwiftValue问题
如果我们存储的不是OC对象,那么objc_getAssociatedObject取出来的值的类型统统为_SwiftValue,直接as? ModelableParameterType.Type绝对是nil,需要在取出来后as AnyObject再转换为其它类型才会成功~~

现在开发者就可以创建一个类来遵守ModelableParameterType协议,并自定义解析键名

struct NetParameter : ModelableParameterType { static var successValue: String { return "false" } static var statusCodeKey: String { return "error" } static var tipStrKey: String { return "errMsg" } static var modelKey: String { return "results" }}

这篇文章我们先从网络层入手,在 iOS
开发中,网络请求与数据解析可以说是其中占比很高并且不可分割的一部分。

二 Moya 简单介绍以及使用

4、插件注入

Q: 厉害了,不过要在什么时机下存储这个自定义键名的NetParameter?A:
额,这个~~~ 哦,对了,可以通过Moya提供的插件机制!

翻出Moya中的Plugin.Swift,找到这个process方法,看看方法说明。

/// 在结束之前,可以被用来修改请求结果/// Called to modify a result before completion.func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>

那好,我们也做一个插件MoyaMapperPlugin给开发者使用,在创建MoyaMapperPlugin时把自定义解析键名的类型传进来

public struct MoyaMapperPlugin: PluginType { var parameter: ModelableParameterType.Type public init<T: ModelableParameterType>(_ type: T.Type) { parameter = type } // modify response public func process(_ result: Result<Response, MoyaError>, target: TargetType) -> Result<Response, MoyaError> { _ = result.map {  -> Response in // 趁机添加相关数据 response.lxf_modelableParameter = parameter return response } return result }}

使用:开发者在创建MoyaProvider对象时,顺便注入插件。(OS:
这一步堪称“注入灵魂”)

MoyaProvider<LXFNetworkTool>(plugins: [MoyaMapperPlugin(NetParameter.self)])

身为一名 iOS 开发,也许你不知道 NSUrlConnection、也不知道
NSURLSession,但你一定知道 AFNetworking /
Alamofire。对他们你肯定也做过一些自己的封装,或者直接采用业内比较知名的第三方封装。比如
Objective-C 中的 YTKNetwork ,Swift 中的 Moya 等等。

1 Moya 简单介绍

Moya是一个基于Alamofire的Networking
library,并且添加了对于ReactiveCocoa和RxSwift的接口支持,大大简化了开发过程,是Reactive
Functional Programming的网络层首选。

Github上的官方介绍罗列了Moya的一些特点:

  • 编译的时候会检查API endpoint
  • 可以用枚举值清楚地定义很多endpoint
  • 增加了stubResponse类型,大大方便了unit testing

5、总结

以上就是主要的踩坑过程了。模型数组解析和指定解析也跟这些差不多的,这里就不再赘述。本人已经将其封装成一个开源库
MoyaMapper,包含了上述已经和未曾说明的功能,下面会讲解如何去使用。以上部分可以称为开胃菜,目的就是平滑过渡到下面MoyaMapper的具体使用。

可能单单使用MoyaMapper的默认子库Core,作用体会上并不会很深。但是,如果你也是使用RxSwift来开发项目的话,请安装'MoyaMapper/Rx'吧,绝对一个字:「爽」

编程 2

MoyaMapper是基于Moya和SwiftyJSON封装的工具,以Moya的plugin的方式来实现间接解析,支持RxSwift

编程 3JSON数据对照

那么问题来了,无论是自己封装也好还是直接采用第三方也好,在我们熟知的
MVC 模式中,你依旧需要在 Controller 中回调 Block / Delegate
对其做出处理,比如对返回数据的校验与解析,对指示器的控制,对刷新控件的控制,把
Model 赋值给 View 等等。而且在 iOS 中 Controller 本身就包含了一个
View,对其生命周期的管理和界面布局无疑又增加了 Controller 的负担。

2 Moya 的 使用

首先我们需要声明一个enum来对请求进行明确分类。

enum APIManager{
    case GetHomeList // 获取首页列表
    case GetHomeDetail(Int)  // 获取详情页
}

然后我们需要让这个enum遵守TargetType协议,在这个协议中可以看到
TargetType定义了我们发送一个网络请求所需要的东西,baseURL,parameter,method等一些计算性属性,我们要做的就是去实现这些东西,当然有带默认值的我们可以不去实现。

extension APIManager: TargetType {
    /// The target's base `URL`.
    var baseURL: URL {
        return URL.init(string: "http://news-at.zhihu.com/api/")!
    }
    /// The path to be appended to `baseURL` to form the full `URL`.
    var path: String {
        switch self {            
        case .GetHomeList: // 不带参数的请求
            return "4/news/latest"
        case .GetHomeDetail(let id):  // 带参数的请求
            return "4/theme/\(id)"
        }
    }
// 区分get 和 post     
    var method: Moya.Method {        
        return .get
    }

    /// The parameters to be incoded in the request.
    var parameters: [String: Any]? {
        return nil
    }
    /// The method used for parameter encoding.
    var parameterEncoding: ParameterEncoding {
        return URLEncoding.default
    }
    /// Provides stub data for use in testing.
    var sampleData: Data {
        return "".data(using: String.Encoding.utf8)!
    }
    /// The type of HTTP task to be performed.
    var task: Task {
        return .request
    }
    /// Whether or not to perform Alamofire validation. Defaults to `false`.
    var validate: Bool {
        return false
    }
}

写好上边的以后 我们就可以去发送一个请求了

private let provider = RxMoyaProvider<APIManager>()

  // 请求数据
        provider
            .request(.GetHomeList)
            .filterSuccessfulStatusCodes()
            .mapJSON().subscribe(onNext: { (json) in                
                print(json)                
            }).addDisposableTo(bag)    

上边就是请求数据了 回调出来json数据

如果对RxSwift还不熟悉的话 建议去看一下之前写的文章RxSwift
个人学习笔记记录
Moya其实是提供了非常方面的RxSwift扩展
简单介绍一下上边方法和变量中的各个名词:

  • RxMoyaProvider是MoyaProvider的子类,是对RxSwift的扩展
  • filterSuccessfulStatusCodes()
    是Moya为RxSwift提供的扩展方法,顾名思义,可以得到成功成功地网络请求,忽略其他的
  • mapJSON() 也是Moya RxSwift的扩展方法,可以把返回的数据解析成 JSON
    格式 会返回一个Observable
    然后我们就可以对这个Observable进行订阅了
    然后我们就可以得到下边的json数据 只展示了部分数据
    网络请求就已经结束了 就这这么简单轻松and easy 😁

{
    date = 20170908;
    stories =     (
                {
            "ga_prefix" = 090817;
            id = 9602715;
            images =             (
                "https://pic2.zhimg.com/v2-3b39b83f560e089a7f6ddb00f6948b81.jpg"
            );
            title = "\U4e24\U5343\U591a\U5e74\U524d\Uff0c\U79e6\U56fd\U4e3a\U4ec0\U4e48\U8981\U7edf\U4e00\U4e2d\U56fd\Uff1f";
            type = 0;
        },
                {
            "ga_prefix" = 090816;
            id = 9601590;
            images =             (
                "https://pic4.zhimg.com/v2-700a8c29d04a885354d78d5a91a9fa5b.jpg"
            );
            title = "\U8fdc\U5904\U6765\U4e86\U8f66\Uff0c\U5148\U89c1\U5230\U8f66\U9876\U5c31\U8bf4\U660e\U5730\U7403\U662f\U5706\U7684\Uff1f\U4e0d\U53ef\U80fd\U7684";
            type = 0;
        },
       );
    "top_stories" =     (
                {
            "ga_prefix" = 090815;
            id = 9607829;
            image = "https://pic3.zhimg.com/v2-14c13f9f87b1b3082929b444072eebb6.jpg";
            title = "\U770b\U7167\U7247\Uff0c\U6211\U5c31\U77e5\U9053\U4f60\U662f\U540c\U6027\U604b\Uff0c\U65af\U5766\U798f\U5927\U5b66\U7684\U4eba\U5de5\U667a\U80fd\U8bf4";
            type = 0;
        },
                {
            "ga_prefix" = 090807;
            id = 9606837;
            image = "https://pic1.zhimg.com/v2-cceffc2e17185ae51b7b2d14b4414e84.jpg";
            title = "\U6211\U8fd9\U4e48\U80d6\Uff0c\U5230\U5e95\U662f\U56e0\U4e3a\U5403\U5f97\U592a\U591a\U8fd8\U662f\U52a8\U5f97\U592a\U5c11\Uff1f";
            type = 0;
        },

    );
}

1、定义并注入自定义键名类

  1. 定义一个遵守ModelableParameterType协议的结构体

// 各参数返回的内容请参考上面JSON数据对照图struct NetParameter : ModelableParameterType { static var successValue: String { return "false" } static var statusCodeKey: String { return "error" } static var tipStrKey: String { return "" } static var modelKey: String { return "results" }}

此外,这里还可以做简单的路径处理,以应付各种情况,以’>’隔开

// 假设返回的json数据关于请求状态的相关数据如下所示,error: { 'errorStatus':false 'errMsg':'error Argument type'}

// 我们指明解析路径:error对象下的errMsg字段,一层层表示下去即可static var tipStrKey: String { return "error>errMsg" }
  1. 以plugin的方式传递给MoyaProvider

// MoyaMapperPlugin这里只需要传入类型MoyaProvider<LXFNetworkTool>(plugins: [MoyaMapperPlugin(NetParameter.self)])

久而久之,当控制器中再加入一些其他的业务逻辑时,整个控制器里的代码就会变得非常臃肿,巨胖无比,随着业务的变更,代码的可读性会变得很差。其实
Controller 中大多数代码都可以被抽离出去,比如说我们的网络请求。

三 ObjectMapper 简单介绍以及使用

json得到了 接下来那就是json转模型了

ObjectMapper 是一个在 Swift 下数据转模型的非常好用,并且很 Swift
的一个框架。以前我们在写 OC 代码的时候用 MJExtension 转模型,到了
Swift 的时代赶紧将 ObjectMapper 使用起来吧。

为了支持映射,类或者结构体只需要实现Mappable协议。这个协议包含两个方法
而且这两个方法是必须实现的

class LLHomeModel: Mappable {

    var date: String?
    var stories: [StoryModel]?
    var top_stories: [StoryModel]?

    //  接下来的两个方法是必须要实现的 
    required init?(map: Map) {        
    }
    public func mapping(map: Map) {
        date <- map["date"]
        stories <- map["stories"]
        top_stories <- map["top_stories"]
    }

}

一旦你的对象实现了 Mappable, ObjectMapper就可以让你轻松的实现和 JSON
之间的转换。
把 JSON 字符串转成 model 对象:

let homeModel = LLHomeModel(JSONString: JSONString)

把一个 model 转成 JSON 字符串:

let JSONString = homeModel.toJSONString(prettyPrint: true)

还有一些具体的基础使用可以参考ObjectMapper中文翻译

2、定义解析模型

创建一个遵守Modelable协议的结构体

struct MyModel: Modelable { var _id = "" ... init(_ json: JSON) { } mutating func mapping(_ json: JSON) { self._id = json["_id"].stringValue ... }}遵守Modelable协议,实现协议的两个方法,在`mapping`方法中描述模型字段的具体解析

本篇文章我们主要是针对 Moya 的再次封装扩展。其实 Moya
本身对网络层的封装已经很优秀了,自带了对于 RxSwift
这类函数响应式库的扩展,网络层非常清晰,并且提供了简单方便的网络单元测试。但我们依然可以把她变得更好。

四 RxSwift+Moya+ObjectMapper优雅的网络请求及模型转换

RxSwift结合MVVM 简直的太合适不过了
我们将 网络请求放在VM里边

    private let provider = RxMoyaProvider<APIManager>()


        // 请求数据
        provider
            .request(.GetHomeList)
            .filterSuccessfulStatusCodes()
            .mapJSON().mapObject(type: LLHomeModel.self).subscribe(onNext: { (model) in                

                self.modelObserable.value = model.stories!                

            }, onError: { (error) in                
            }).addDisposableTo(bag)

可以看到我们上边代码中
provider .request(.GetHomeList) .filterSuccessfulStatusCodes() .mapJSON().
这个方法本身应该得到 JSON的 但是我后边跟了一个mapObject 的方法
这个方式可以直接根据json的格式转换成模型 或者是模型数组 来看一下这个方法
我是单独定义了一个json转模型的类LLToModelExtension.swift

extension Observable{
    func mapObject<T:Mappable>(type: T.Type) -> Observable<T> {
        return self.map { response in
            guard let dict = response as? [String : Any] else{
                throw RxSwiftMoyaError.ParseJSONError
            }
            return Mapper<T>().map(JSON: dict)!
        }
    }

    func mapArray<T: Mappable>(type: T.Type) -> Observable<[T]> {
        return self.map { response in
            //if response is an array of dictionaries, then use ObjectMapper to map the dictionary
            //if not, throw an error
            guard let array = response as? [Any] else {
                throw RxSwiftMoyaError.ParseJSONError
            }

            guard let dicts = array as? [[String: Any]] else {
                throw RxSwiftMoyaError.ParseJSONError
            }            
            return Mapper<T>().mapArray(JSONArray: dicts)
        }
    }
}

enum RxSwiftMoyaError: String {
    case ParseJSONError
    case OtherError
}
extension RxSwiftMoyaError: Swift.Error { }

介绍一下上边代码中各个方法以及名词

  • 1 mapObject 方法是处理单个对象的 mapArray 处理对象数组
  • 2 如果传进来的数据 是一个NSDictionary 的话 那么就利用
    ObjectMappermap 方法映射这些数据,这个方法会调用你之前在
    mapping 方法里面定义的逻辑。
  • 3 如果 response 不是一个 dictionary, 那么就抛出一个错误。
  • 4 在底部自定义了简单的 Error,继承了 Swift 的 Error
    类,在实际应用过程中可以根据需要提供自己想要的 Error。

彩蛋

可能会有人问 为什么请求回来的数据 要赋值给modelObserable.Value呢
而不是赋值给一个模型数组 然后reloadData呢

这里我用的RXSwift里边UItableView绑定数据的一个方法
再也不用写一大串数据源方法了
这个也可以去这里参考哦

      var modelObserable = Variable<[StoryModel]> ([])

        //MARK: Rx 绑定tableView数据
        modelObserable.asObservable().bind(to: tableV.rx.items(cellIdentifier: cellID, cellType: LLHomeCell.self)){ row , model , cell in            
            cell.titleLbl.text = model.title            
            cell.imageV?.kf.setImage(with: URL.init(string: (model.images?.count)! > 0 ? (model.images?.first)! : ""))            
            }.addDisposableTo(bag)

Swift项目框架地址

参考文章
Moya入坑记
RxSwift+Moya

3、解析数据

// Resultpublic func mapResult(params: ModelableParamsBlock? = nil) -> MoyaMapperResult// Modelpublic func mapObject<T: Modelable>(_ type: T.Type, modelKey: String? = nil) -> T// Result+Modelpublic func mapObjResult<T: Modelable>(_ type: T.Type, params: ModelableParamsBlock? = nil) -> (MoyaMapperResult, T)// Modelspublic func mapArray<T: Modelable>(_ type: T.Type, modelKey: String? = nil) -> [T]// Result+Modelspublic func mapArrayResult<T: Modelable>(_ type: T.Type, params: ModelableParamsBlock? = nil) -> (MoyaMapperResult, [T])

编程,上面的五个方法,观其名,知其意,这里就不过多解释了,主要注意两点:

  • result

// 元祖类型// 参数1:根据statusCodeKey取出的值与successValue是否相等// 参数2:根据tipStrKey取出的值result:(Bool, String)
  • params

// params: ModelableParamsBlock? = nil// 这里只有在特殊场景下才需要使用到。如:项目中需要在某处使用特定接口,但是返回的json格式跟自己项目的不一样,并且只有这么一两处用得着该额外接口,那就需要我们这个参数了,以Block的方式返回解析参数类型。

// Modelpublic func toJSON(modelKey: String? = nil) -> JSON// 获取指定路径的值public func fetchJSONString(path: String? = nil, keys: [JSONSubscriptType]) -> String

这两个方法,如果没有指定路径,默认都是针对modelKey的

// fetchJSONString(keys: <[JSONSubscriptType]>)1、通过 keys 传递数组, 该数组可传入的类型为 Int 和 String2、默认是以 modelKey 所示路径,来获取相应的数值。如果modelKey并非是你所想要使用的解析路径,可以使用下方的重载方法重新指定路径即可// response.fetchJSONString(path: <String?>, keys: <[JSONSubscriptType]>)

MoyaMapper也提供了Rx子库,为方便RxSwift的流式编程下便捷解析数据

MoyaMapper默认只安装Core下的文件pod 'MoyaMapper'RxSwift拓展pod 'MoyaMapper/Rx'

具体使用还不是很明白的同学可以下载并运行Example看看

如果MoyaMapper有什么不足的地方,欢迎提出issues,感谢大家的支持

封装 Moya

Moya 的使用我在这里就不贴了,没用过的小伙伴可以去官方文档学习一下。

用过的小伙伴知道,我们使用 Moya 都要先创建一个 Enum 遵守 TargetType
协议实现对应的方法(比如指定请求的 URL 路径,参数等等)。

public enum GitHub { case userProfile}

extension GitHub: TargetType { public var baseURL: URL { return URL(string: "https://api.github.com")! } public var path: String { switch self { case .userProfile: return "/users/\(name.urlEscaped)" } } public var method: Moya.Method { return .get } public var task: Task { switch self { default: return .requestPlain } }}

而实际的请求是使用 MoyaProvider<Target> 类,传入一个遵守 TargetType
协议的 Enum,创建 MoyaProvider 对象去请求的。

provider = MoyaProvider<GitHub>()provider.request(.userProfile("InsectQY")) { result in // do something with the result}

可是如果把项目中所有的网络请求都写在同一个 Enum
中的话,这个Enum里的代码会非常多,维护起来也并不方便。

笔者在使用时通常都是根据模块创建多个
Enum,比如按首页模块,新闻模块这样划分。如果这么写的话,我们创建
MoyaProvider 对象时就不能再传入指定类型的 Enum
了。我们把创建对象的写法改成 MoyaProvider<MultiTarget>,所有传入的
Enum 得用 MultiTarget 包装一层。

let provider = MoyaProvider<MultiTarget>provider.request(MultiTarget(GitHub.userProfile("InsectQY"))) { result in // do something with the result}

看了上面的代码,好像已经开始变得不那么优雅了,我指定一个请求竟然要写这么多代码,一大堆括号看的眼睛都晕。能不能直接使用
Enum 的类型不需要借助 MoyaProvider 对象去请求呢,类似这样的效果。

GitHub.userProfile("InsectQY").request

以下封装我们基于 RxSwift 来实现,当然如果你不熟悉 RxSwift
也没关系,这里只是对封装思路的介绍,封装完成以后可以直接使用,等以后熟悉了
RxSwift 再回头看也行。以下文章的思路大多借鉴 RxNetwork 这个库的实现。

首先我们为 TargetType 添加自己的 public extension 方便外界调用。

public extension TargetType {}

先实现一个可以直接使用 Enum 类型调用请求的方法。

let provider = MoyaProvider<MultiTarget>public extension TargetType { func request() -> Single<Response> { return provider.rx.request(.target }}

这个方法返回一个 Single 类型的 ObservableSingleObservable
的另一个版本。它不像 Observable
可以发出多个元素,它要么只能发出一个元素,要么产生一个 error
事件,不共享状态变化,用来做请求的返回非常合适。

写完我们就可以直接用 Enum
调用请求,怎么样是不是非常简单呢。代码的可读性也变高了很多。对请求的结果只需要调用
subscribe 去监听即可。

GitHub.userProfile("InsectQY").request.subscribe...

封装 JSON 解析

先回顾一下我们以往的 JSON
解析,通常都是使用第三方解析库,直接把代码放到每次请求的回调中去处理。

乍一看其实没毛病,那么这么做有什么弊端呢?其实这种写法侵入性很强,试想一下假如有一天你这个第三方解析库不维护了,或者种种原因你需要更换到其他的第三方,或者自己手写解析,那么你需要替换和修改的地方就非常多。

你可能会说,那我可以在第三方解析的方法上封装一层,然后调用我自己的解析方法啊。是的,想法很好,但你有没有想过其实解析的写法可以变得非常优雅。

Moya 自身就提供了基于 Codable 协议的原生解析方法。

public func map<D>(_ type: D.Type, atKeyPath keyPath: String? = default, using decoder: JSONDecoder = default, failsOnEmptyData: Bool = default) throws -> D where D : Decodable

支持对 JSON
指定路径的解析,实现的原理也非常简单,感兴趣的小伙伴可以去源码中学习一下。具体位置在
Response
这个类中搜索关键词即可。这个方法我们直接就能使用,转模型的代码可以写成这样

GitHub.userProfile("InsectQY").request.map(UserModel.self)

当然最好我们还是在原生方法上再封装一层,减少原生方法对项目的侵入性。

需要注意的是,在我们平时使用 Codable
协议时,通常都要分清解析的是数组还是字典。如果是数组类型数据的话,必须得调用指定解析数组的方法,否则无法正确解析。

但 Moya 是可以在外界直接传入数组类型的,具体实现也非常简单。用一个
Struct
的结构体去包装每次需要解析的对象,再把解析对象指定为包装好的结构体。

private struct DecodableWrapper: Decodable { let value: T}

这样就不用关心外界需要解析的具体类型,相当于每次解析的必然是一个包装好的字典类型,最后只要把结构体里的
value 返回就行。

扯一个题外话,那这种实现思路在 Objective-C
中是否可行呢,可以思考如下两个问题。

  1. Objective-C 中我们使用 MJExtension / YYModel 这些库去解析
    JSON
    时,都要调用指定的解析方法(数组和字典的解析方法是不同的),能否用以上的思路把解析数组和解析字典的方法整合成一个方法呢?
  2. 如果要解析的模型中有个数组属性,数组里面又要装着其他模型。还要写指定数组内部类型的方法。
  • MJExtension

// Tell MJExtension what type of model will be contained in statuses and ads.[StatusResult mj_setupObjectClassInArray:^NSDictionary *{ return @{ @"statuses" : @"Status", // @"statuses" : [Status class], @"ads" : @"Ad" // @"ads" : [Ad class] };}];
  • YYModel

+ (NSDictionary *)modelContainerPropertyGenericClass { // value should be Class or Class name. return @{@"shadows" : [Shadow class], @"borders" : Border.class, @"attachments" : @"Attachment" };}

这么写目的是为了在运行时拿到数组中元素的具体类型,再用 Runtime
去类中获取属性以及 KVC
赋值。如果用泛型指定数组里元素的具体类型的话,这些方法是否可以省略呢?

然而很遗憾,原生的 Objective-C 是无法实现以上想法的。原因在于
Objective-C
的泛型只能算是”伪”泛型,仅仅是一个编译器特性,只能在编译时为 Xcode
提供具体类型,在运行时是没有的。

封装网络缓存

为了提升用户体验,在实际开发中,有一些内容可能会加载很慢,我们想先显示上次的内容,等加载成功后,再用最新的内容替换上次的内容。也有时候,由于网络处于断开状态,为了更加友好,我们想显示上次缓存中的内容。

网络缓存我们基于 Cache 来实现。首先创建一个 CacheManager
统一处理所有的读取和存储操作。我们把读取模型数据和读取网络请求返回的
Response 数据分别创建不同的方法(这里只贴了模型的方法)。

// MARK: - 读取模型缓存 static func object<T: Codable>(ofType type: T.Type, forKey key: String) -> T? { do { let storage = try Storage(diskConfig: DiskConfig(name: "NetObjectCache"), memoryConfig: MemoryConfig(), transformer: TransformerFactory.forCodable(ofType: type)) try storage.removeExpiredObjects() return (try storage.object(forKey: key)) } catch { return nil } } // MARK: - 缓存模型 static func setObject<T: Codable>(_ object: T, forKey: String) { do { let storage = try Storage(diskConfig: DiskConfig(name: "NetCache"), memoryConfig: MemoryConfig(), transformer: TransformerFactory.forCodable(ofType: T.self)) try storage.setObject(object, forKey: forKey) } catch { print("error\ } }

缓存的方法封装好以后,我们还需要知道缓存的 key,这里我们采用请求的
URL + 参数拼接成 key

extension Task { public var parameters: String { switch self { case .requestParameters(let parameters, _): return "\(parameters)" case .requestCompositeData(_, let urlParameters): return "\(urlParameters)" case let .requestCompositeParameters(bodyParameters, _, urlParameters): return "\(bodyParameters)\(urlParameters)" default: return "" } }}

public extension TargetType { var cachedKey: String { return "\(URL(target: self).absoluteString)?\(task.parameters)" }}

万事俱备,现在为 TargetType 添加一个 cache 属性,返回一个
Observable 包装遵守 TargetType 协议的 Enum

var cache: Observable<Self> { return Observable.just }

那么我们调用缓存的代码就变成了这样

GitHub.userProfile("InsectQY").cache

但是这个缓存还没有具体的实现,现在我们为缓存添加实现,只有遵守
TargetType 协议才能调用。

每次调用方法都把请求结果缓存到本地,返回数据时先从本地获取,本地没有值时只返回网络数据。这里的
startWith 保证本地数据有值时,本地数据每次都优先在网络数据之前返回。

extension ObservableType where E: TargetType { public func request() -> Observable<Response> { return flatMap { target -> Observable<Response> in let source = target.request().storeCachedResponse(for: target).asObservable() if let response = target.cachedResponse { return source.startWith } return source } }}

现在我们的缓存已经初步完成了,在 onNext
回调中,第一次返回的是本地数据,第二次是网络数据。我们的请求就变成了这样

GitHub.userProfile("InsectQY").cache.request().map(UserModel.self).subscribe ...

这样的好处是,每个方法之间都是独立的,我不想要缓存我只要去掉 cache
不想转模型只要去掉 map ,整段代码的可读性变得很强。

由于 RxSwift 的存在,你也不需要在 Controller
销毁时去手动管理网络请求的取消。你想做一些网络的其他高级操作也变得非常容易,比如说链式的网络请求,group
式的网络请求,请求失败自动重试,同一个请求多次请求时短时间忽略相同的请求等等都非常简单。

现在回头看看我们的需求,优先展示本地数据,网络数据返回时自动替换本地数据,网络请求失败时加载本地数据。

但是这种写法应用场景相对比较单一,只能适用于本地数据和网络数据的处理是相同的情况。我们在
onNext
中无法区分本地数据和网络数据,假如想对本地数据做一些特殊处理的话是不行的。我们再完善一下代码,将本地数据的回调告诉外界。

func onCache<T: Codable>(_ type: T.Type, atKeyPath keyPath: String? = "", _ onCache:  ->  -> OnCache<Self, T> { if let object = cachedObject {onCache?} return OnCache}

返回的 OnCache 对象是自定义的一个结构体

public struct OnCache<Target: TargetType, T: Codable> { public let target: Target public let keyPath: String init(_ target: Target, _ keyPath: String) { self.target = target self.keyPath = keyPath } public func request() -> Single<T> { return target.request() .mapObject(T.self, atKeyPath: keyPath) .storeCachedObject(for: target) }}

现在我们就可以在 onCache
的回调中拿到本地数据了,如果你想对本地数据做一些自己的操作和处理的话,选择第二种方案会更加合适。后续的
subscribe 监听到的是一个 Single
,如之前所说,只会返回成功或者失败,这里我们只把网络数据返回就好。这样就做到了网络数据和本地数据的区分。

GitHub.userProfile("InsectQY").onCache(UserModel.self, {  in }).request().subscribe ...

好了看了以上这么多,我们只是对网络层做了一些封装,还没有做这种写法实际在项目中的应用,后续将教大家如何用
RxSwift 减少控制器的代码。

具体的 demo 和用法可以查看我开源的这个项目 GamerSky或者原作者的
RxNetwork 。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图
Copyright @ 2010-2019 澳门新葡亰官网app 版权所有