基于 Rx 的网络层实践

作者:李富强投稿发布于本专栏

日常开发中,API请求的管理是我们无法回避的一个难题,相对于web,客户端的网络请求处理要更加复杂,例如异步、通用参数、数据模型化、通用错误处理、可取消。更高一些的用户体验要求是,不阻塞用户的操作,随时可以退出以及重试,这也就要求我们对网络请求的各个状态处理都要非常完善,这就需要一个非常完善的网络层提供支撑。

网络框架

Apple 在 iOS SDK 中是为我们提供了 HTTP 网络请求接口的,从 CFNetwork 到 NSURLConnection,以及最近的 NSURLSession,但这些框架我们很少直接去使用。因为官方的 SDK 只提供了最基础的接口,而我们实际业务开发中,网络层需要提供更多的功能,才能提高业务开发效率。下面简单举几个例子说明一下为什么需要框架。

第一,HTTP 参数的处理,例如GET的请求参数在经过 URL Encoding 之后添加 URL 的 query 中,POST 请求的包体组装( multi-part 或者 form ),都是比较繁琐但是相对通用的逻辑,这些都应该在网络层处理之后,让业务层可以无感知地去使用。

第二,返回数据的处理,例如,我们指定服务器返回 JSON 数据,框架会把这些信息处理添加到 HTTP 协议中,我们的业务层逻辑就会很简单,获得可以序列化的数据或者直接报错。

第三,请求状态的管理,例如,限制请求最大并发数、网络请求的取消等。如果使用 NSURLConnection 。还有一个比较经典的示例,就是 NSURLConnection 需要 runloop 机制的支持才能处理回调,如果你启动一个线程,但是没有启动它的 runloop,你在这个线程中发起网络请求的话,这个请求不会产生回调。如果我们把所有的 NSURLConnection 都放到主线程中发起,这会产生一个其他问题,比如主线程的负担过重。

由于这些需求的存在,iOS 开发中诞生了几乎是事实标准的网络框架,早期的 ASIHTTPRequest,这个框架是基于 CFNetwok 直接实现的,逻辑非常复杂,从我开始使用到结束使用,也只是看懂了大概的架构,但是细节一直没有深入探究。ASI 使用底层技术的好处在于性能的确较高。相对于 AFNetworking ,ASI 提供了更多的功能支持,例如大文件下载、同步请求。后续因为作者不再维护,同时暴露出了一些安全问题,以及 AFNetworking 的流行,慢慢大家就不再使用了。Objective-C 时代,AFNetworking 几乎是事实上的网络框架SDK,我个人感觉,最主要的原因是因为简单,AFN 2.x 中 NSURLConnection + runloop、NSOperation、HTTP 参数的处理等,都是非常优雅的代码,建议每个 iOS 开发人员都应该看看。

我们上面提到的一些网络层的需求,AFNetworking 都帮我们进行了处理,但是一般来说,在 AFNetworking 这类网络框架的基础上,大家往往还会再做一层封装,一方面适应自己的业务需求,另外一方面是考虑到后续框架的变迁而隐藏底层网络框架,比如 AFNetworking 从 2.x 到 3.x 的变迁等。我个人的封装方式是定义一个 Request 类,隐藏底层的 NSURLSessionTask 或者 NSOperation。例如:

  • class CusomRequest {
  • var task: URLSessionTask?
  • let url: URL
  • init(method: Method, url: URL, parameters: [String: Any]? = nil, headers: [String: Any]? = nil) {
  • // ...
  • }
  • func start(callback: CompletionClosure) {
  • // ...
  • }
  • func cancel() {
  • }
  • }

Rx

当Swift第一版本发布的时候,我快速学习了一下,但是非常失望,感觉这是一个半成品,很多基本的特性都无法支持。后续断断续续写了一些小东西,Swift 3.0 之后,终于感觉比较成熟。公司内部的项目,由于基础库庞大、包大小的限制等原因,一直无法实践 Swift 。但是我业余的 night job 基本上都在使用 Swift,在做小东西的过程中,也需要设计网络层,当我去探索的时候,发现了 RxAlamofire 这个效率极高的框架,把 RxSwift 和 Alamofire 结合到了一起。

简单介绍一下我对 Reactive Programming 的理解,最开始接触这个概念是从ReactiveCocoa 开始的,后续也经常使用,感觉非常方便。虽然 ReactiveCocoa 也有 Swift 版本,但是当我开始阅读 Rx 官方的文档之后,就果断选择了 RxSwift 替代 ReactiveCocoa,我个人的理解有三方面:

  • 首先,我认为 Rx 的文档质量非常优秀,与学习 ReactiveCocoa 时资料匮乏形成鲜明对比,大家只需要看官网的文档就能无障碍理解和使用。
  • 其次,我认为 RxSwift 的概念上更好理解,Observable 与 Observer 的关系非常清晰,Obervable 生产 data flow,经过各种 Operators 变化,最终提供给Observer 消费。官网的一段话: In ReactiveX an observer subscribes to an Observable.
  • 最后,Rx 支持很多语言,并且概念上都是一致的,让我们可以做到 Learn Once, Write Everywhere,我自己尝试过 RxJava,除了一些语法上不一致,其他概念理解起来毫无障碍。甚至 RxJS 在前端领域也越来越流行,这个诱惑很致命的。

我认为对 Rx 设计思想最好的解释是 Rx 官网 (http://reactivex.io/) 的一段话:ReactiveX is a combination of the best ideas from the Observer pattern, the Iterator pattern, and functional programming。三种设计模式或者思想,观察者模式,迭代器模式,函数式编程。

针对 Rx,我个人的观点是 Rx 抹平了调用接口,无论是进行网络请求,还是定位,甚至 UI 变化,业务层不再需要了解API的具体调用方式,只要服务层封装到位,对业务层而言,只用获取一个 Observable 处理即可,这就是我认为的最大的优势,再加上比较通用化的 Operators,更能简化客户端复杂的数据流操作。举个简单的例子,定位完成之后,发起多个网络请求,请求全部成功之后,页面数据才能显示,中间任何一个步骤出现错误都会中断。如果使用 Rx,你会发现这些问题可以大大简化,而使用异步回调,会需要非常多的变量来辅助处理。

网络层

解决的问题

选定网络框架之后,就需要搭建自己项目的网络层实现。在网络库的基础上,我们之所以为项目再设定一个网络层,除了我们之前提到的隐藏底层网络框架细节之外,还主要为了解决几个问题。首先,通用参数,例如设备唯一标识、登录用户 token 信息、反爬虫签名、设备的基本信息等。

其次, 统一的错误处理,例如底层网络 HTTP 发生错误、JSON 解析或者数据格式产生错误等,我们可以与 PM 约定较为用户友好的提示,在网络层统一处理。否则业务层在处理网络数据的时都需要再做重复这样的逻辑,有洁癖的人应该都无法这种 copy-paste 模式的重复代码。

最后,统一的业务数据处理。一般来说,API 服务器返回的 JSON 数据都有通用的结构。例如

  • {
  • "code": 200,
  • "message": "请求已成功提交",
  • "data": {
  • "id": 111,
  • "nick_name": "大强",
  • "avatar": "http://xxx.png"
  • }
  • }

假设两端服务器与客户端约定,code 等于200表示业务处理成功,其他值表示错误码,那我们就可以在网络层判断 code 值,如果不等于正常值,直接返回 error,业务层的重复逻辑就会再次减少。同时,服务器返回的数据都在 data 字段,在确认 code 等于200,即业务处理成功之后,我们可以把模型化逻辑也在网络层统一处理。经过这两个逻辑的处理,业务层的逻辑进一步简化,失败返回 error,成功返回数据模型。

实现

我们最终的目的是业务层接口尽量简单,处理的逻辑尽可能少,先看看我们最终要达到的效果是什么,个人主页请求用户信息,我会在代码注释中为大家简单解释一下这段代码:

  • /// 网络层
  • extension Network{
  • func userDetail(userid: Int) -> Observable<User> {
  • let url = URL(string: baseUrl + "/user/detail.do")!
  • let parameters: [String: Any] = ["userid": userid]
  • return rx_json(.get, url, parameters: parameters).data()
  • }
  • }
  • /// 业务层
  • Network.default.userDetail(userid: self.userId)
  • .subscribe(
  • onNext:{ [weak self] (user) in
  • /// 处理正常返回的业务数据
  • guard let `self` = self else {return}
  • self.user = user
  • self.refreshUI()
  • },
  • onError: { [weak self] (error) in
  • /// 处理错误信息
  • Toast(text: error.localizedDescription).show()
  • self?.navigationController?.popViewController(animated: true)
  • }
  • )
  • /// 假设网络请求耗,而用户此时退出页面,这行代码会把请求cancel,避免网络资源的浪费
  • .addDisposableTo(disposeBag)

看完了最终效果,我们开始架构我们的整个网络框架,首先是 Network 对象,用于管理网络请求的单例对象。下面这段代码解决了上面提到的网络层的第一个问题,通用参数的处理。

  • class Network {
  • /// 单例
  • static let `default`: Network = {
  • return Network()
  • }()
  • // 主机地址
  • let baseUrl = "http://example.com"
  • /// JSON解析的通用key值
  • static let ok = 200
  • static let codeKey = "code"
  • static let messageKey = "message"
  • static let dataKey = "data"
  • static let cursorKey = "cursor"
  • /// 添加通用参数
  • func commonParameters(parameters: [String: Any]?) -> [String: Any] {
  • var newParameters: [String: Any] = [:]
  • if parameters != nil {
  • newParameters = parameters!
  • }
  • /// 添加通用参数
  • /// 用户token参数
  • if let token = AccountCenter.default.user?.token {
  • newParameters["token"] = token
  • }
  • /// App版本信息
  • if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") {
  • newParameters["app_version"] = version
  • }
  • return newParameters
  • }
  • /// 统一的API请求入口
  • func rx_json(_ method: Alamofire.HTTPMethod,
  • _ url: URLConvertible,
  • parameters: [String: Any]? = nil,
  • encoding: ParameterEncoding = URLEncoding.default,
  • headers: [String: String]? = nil)
  • -> Observable<JSON> {
  • return string(
  • method,
  • url,
  • parameters: commonParameters(parameters: parameters),
  • encoding: encoding,
  • headers: headers
  • )
  • .common()
  • }
  • }

这就是网络层最基础的代码,不知道大家有没有注意 rx_json 最后一行代码。Observable<String>.common()方法,把 string 函数返回的 Observable<String> 类型转换为了 Observable<JSON>类型,这个是通过为 Observable添加 extension 实现的,这里不仅仅是简单的类型转换,还包含了我们之前所说的通用的网络错误处理。在具体实现之前,我们可以简单定义一下基本的网络错误码和提示语:

  • enum ErrorCode: Int {
  • case `default` = -1212
  • case json = -1213
  • case parameter = -1214
  • }
  • enum ErrorMessage: String {
  • case `default` = "网络状态不佳,请稍候再试!"
  • case json = "服务器数据解析错误!"
  • case parameter = "参数错误,请稍候再试!"
  • }
  • extension NSError {
  • class func network(reason: String, code: Int) -> NSError {
  • let userInfo = [NSLocalizedDescriptionKey: reason.localized,
  • NSLocalizedFailureReasonErrorKey: reason.localized]
  • return NSError(domain: "com.example.networkerror", code: code, userInfo: userInfo)
  • }
  • }

下面是 common 方法的实现,我将会在代码注释中,为大家详细讲解这段代码:

  • /*
  • 这段代码之所以这么写,是因为Swift中,当我们为某个有泛型的类添加extension时,不支持限定泛型为继承自某个类,例如:
  • extension Observable where Element == String {}
  • 这段代码会报错,因为我们无法限定Element继承自String或就是String类,但是可以限定Element实现了某个protocol。所以我们可以通过protocol extension的方式绕过去,参考:http://www.marisibrothers.com/2016/03/extending-swift-generic-types.html。
  • */
  • protocol StringProtocol {}
  • extension String : StringProtocol {}
  • extension Observable where Element: StringProtocol {
  • func common() -> Observable<JSON> {
  • return self
  • .catchError({ (error) -> Observable<Element> in
  • /// 这里统一处理更加底层的网络错误,比如服务器404或者500等常见错误。
  • /// 将底层网络框架返回的错误提示,在这里转换为更加友好的用户提示,然后把错误抛给业务层进行处理。
  • return Observable<Element>.create({ (observer) -> Disposable in
  • observer.on(.error(NSError.network(reason: ErrorMessage.default.rawValue, code: ErrorCode.default.rawValue)))
  • return Disposables.create()
  • })
  • })
  • .flatMap { (element) -> Observable<JSON> in
  • let string = element as! String
  • /// 通用的业务数据处理逻辑,将服务器返回的字符串进行解析
  • /// 获取JSON数据中的code字段,判断业务请求是否成功,即code是否等于200
  • return Observable<JSON>.create({ (observer) -> Disposable in
  • /// 解析JSON
  • let json = JSON.parse(string)
  • if let code = json[Network.codeKey].int, code == Network.ok {
  • /// 业务请求成功,把解析后的JSON数据传递下去
  • observer.on(.next(json))
  • observer.on(.completed)
  • } else {
  • /// 如果业务请求失败,则处理错误信息后,把错误抛到业务层处理
  • var reason = ErrorMessage.default.rawValue
  • if let message = json[Network.messageKey].string {
  • reason = message
  • }
  • var code = ErrorCode.default.rawValue
  • if let c = json[Network.codeKey].int {
  • code = c
  • }
  • observer.on(.error(NSError.network(reason: reason, code: code)))
  • }
  • return Disposables.create()
  • })
  • }
  • .observeOn(MainScheduler.instance) /// 确定业务层的订阅发生在主线程,避免在子线程操作UI
  • }
  • }

经过上面的处理,我们处理了底层网络错误以及基本的业务数据的处理,这时我们获得了解析之后的 JSON 数据。但是一般来说,我们业务层更倾向于使用模型化之后的数据,在 OC 时代,这个模型化通常会比较麻烦,基本上所有的网络请求都需要把这个逻辑重复一遍。下面是我在 Swift 中的实现,同样,我会通过代码注释的方式为大家解释这段代码:

  • /// 依旧是上面的原因,我们需要为JSON定义一个protocol,来达到限定泛型的类型为JSON类的目的
  • protocol JSONProtocol {}
  • extension JSON: JSONProtocol{}
  • extension Observable where Element: JSONProtocol {
  • /// 如果返回数据是列表信息,在这个方法中解析列表数据,并将列表数据模型化为模型数组,这里使用 T 代表模型类。
  • typealias ListType<T> = ([T], String?)
  • /// 限定T模型类实现了 ObjectMapper 中的 BaseMappable protocol,通过这个protocol就能直接进行模型化
  • func list<T: BaseMappable>(callback: ((T) -> Void)? = nil) -> Observable<ListType<T>> {
  • return self.flatMap{ (element) -> Observable<ListType<T>> in
  • let json = element as! JSON
  • return Observable<ListType<T>>.create { (observer) -> Disposable in
  • /// 从json数据的data字段中取出列表数据,并使用ObjectMapper的方法把数据模型化
  • if let data = json[Network.dataKey].arrayObject, let array = Mapper<T>().mapArray(JSONObject: data) {
  • /// 返回模型类数组以及服务器返回的用于分页的cursor
  • observer.on(.next((array, json[Network.cursorKey].string)))
  • observer.on(.completed)
  • } else {
  • /// 解析错误,向业务层抛出通用的json解析错误
  • observer.on(.error(NSError.network(reason: ErrorMessage.json.rawValue, code: ErrorCode.json.rawValue)))
  • }
  • return Disposables.create()
  • }
  • }
  • }
  • /// 如果服务器返回的数据中,data对应的是某个数据模型,直接调用这个方法即可获得模型化之后的数据
  • func data<T: BaseMappable>(callback: ((T) -> Void)? = nil) -> Observable<T> {
  • return self.flatMap { (element) -> Observable<T> in
  • let json = element as! JSON
  • return Observable<T>.create { (observer) -> Disposable in
  • if let data = json[Network.dataKey].dictionaryObject, let object = Mapper<T>().map(JSON: data) {
  • if let callback = callback {
  • callback(object)
  • }
  • observer.on(.next(object))
  • observer.on(.completed)
  • } else {
  • /// 解析错误,向业务层抛出通用的json解析错误
  • observer.on(.error(NSError.network(reason: ErrorMessage.json.rawValue, code: ErrorCode.json.rawValue)))
  • }
  • return Disposables.create()
  • }
  • }
  • }
  • /// 针对只处理状态码的返回数据,使用这个bool()方法处理,返回的bool值只有true,其他情况都是失败
  • func bool(callback: (() -> Void)? = nil) -> Observable<Bool> {
  • return self.flatMap { (element) -> Observable<Bool> in
  • return Observable<Bool>.create { (observer) -> Disposable in
  • if let callback = callback {
  • callback()
  • }
  • observer.on(.next(true))
  • observer.on(.completed)
  • return Disposables.create()
  • }
  • }
  • }
  • }

上面的代码,我利用图虫的API,简单写了一个示例供大家参考,https://github.com/lihei12345/tuchongapp

总结

经过上面的步骤,我们介绍了 UIKit 提供的SDK和第三方网络框架,基于他们,最后我们实现了一个网络层,当然这是比较简化的网络层。对于比较复杂和大型的 App 而言,功能可能远远不止这些。通过 Rx,我们不需要再封装 CustomRequest 对象,不需要在业务层在写一堆判断逻辑,Rx 把数据流变得异常清晰 -- 业务数据或错误。这篇文章,我主要想为大家演示了 Swift 的语法带来的新的可能性,我们要避免使用 Objective-C 的思维写 Swift,才能真正发挥 Swift 这个现代语言带来的优势。

© 著作权归作者所有
这个作品真棒,我要支持一下!
本专栏文章由 @故胤道长、@一缕殇流化隐半边冰霜、@没故事的卓同学、@Onetaway 编辑。关于这本书的任何的意...
0条评论
top Created with Sketch.