作者:李富强投稿发布于本专栏
日常开发中,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表示业务处理成功,其他值表示错误码,那我们就可以在网络层判断 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)
}
)
.addDisposableTo(disposeBag)
看完了最终效果,我们开始架构我们的整个网络框架,首先是 Network 对象,用于管理网络请求的单例对象。下面这段代码解决了上面提到的网络层的第一个问题,通用参数的处理。
class Network {
static let `default`: Network = {
return Network()
}()
let baseUrl = "http://example.com"
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!
}
if let token = AccountCenter.default.user?.token {
newParameters["token"] = token
}
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") {
newParameters["app_version"] = version
}
return newParameters
}
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 方法的实现,我将会在代码注释中,为大家详细讲解这段代码:
protocol StringProtocol {}
extension String : StringProtocol {}
extension Observable where Element: StringProtocol {
func common() -> Observable<JSON> {
return self
.catchError({ (error) -> Observable<Element> in
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
return Observable<JSON>.create({ (observer) -> Disposable in
let json = JSON.parse(string)
if let code = json[Network.codeKey].int, code == Network.ok {
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)
}
}
经过上面的处理,我们处理了底层网络错误以及基本的业务数据的处理,这时我们获得了解析之后的 JSON 数据。但是一般来说,我们业务层更倾向于使用模型化之后的数据,在 OC 时代,这个模型化通常会比较麻烦,基本上所有的网络请求都需要把这个逻辑重复一遍。下面是我在 Swift 中的实现,同样,我会通过代码注释的方式为大家解释这段代码:
protocol JSONProtocol {}
extension JSON: JSONProtocol{}
extension Observable where Element: JSONProtocol {
typealias ListType<T> = ([T], String?)
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
if let data = json[Network.dataKey].arrayObject, let array = Mapper<T>().mapArray(JSONObject: data) {
observer.on(.next((array, json[Network.cursorKey].string)))
observer.on(.completed)
} else {
observer.on(.error(NSError.network(reason: ErrorMessage.json.rawValue, code: ErrorCode.json.rawValue)))
}
return Disposables.create()
}
}
}
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 {
observer.on(.error(NSError.network(reason: ErrorMessage.json.rawValue, code: ErrorCode.json.rawValue)))
}
return Disposables.create()
}
}
}
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 这个现代语言带来的优势。
作者:李富强投稿发布于本专栏
日常开发中,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表示业务处理成功,其他值表示错误码,那我们就可以在网络层判断 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)
}
)
.addDisposableTo(disposeBag)
看完了最终效果,我们开始架构我们的整个网络框架,首先是 Network 对象,用于管理网络请求的单例对象。下面这段代码解决了上面提到的网络层的第一个问题,通用参数的处理。
class Network {
static let `default`: Network = {
return Network()
}()
let baseUrl = "http://example.com"
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!
}
if let token = AccountCenter.default.user?.token {
newParameters["token"] = token
}
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") {
newParameters["app_version"] = version
}
return newParameters
}
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 方法的实现,我将会在代码注释中,为大家详细讲解这段代码:
protocol StringProtocol {}
extension String : StringProtocol {}
extension Observable where Element: StringProtocol {
func common() -> Observable<JSON> {
return self
.catchError({ (error) -> Observable<Element> in
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
return Observable<JSON>.create({ (observer) -> Disposable in
let json = JSON.parse(string)
if let code = json[Network.codeKey].int, code == Network.ok {
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)
}
}
经过上面的处理,我们处理了底层网络错误以及基本的业务数据的处理,这时我们获得了解析之后的 JSON 数据。但是一般来说,我们业务层更倾向于使用模型化之后的数据,在 OC 时代,这个模型化通常会比较麻烦,基本上所有的网络请求都需要把这个逻辑重复一遍。下面是我在 Swift 中的实现,同样,我会通过代码注释的方式为大家解释这段代码:
protocol JSONProtocol {}
extension JSON: JSONProtocol{}
extension Observable where Element: JSONProtocol {
typealias ListType<T> = ([T], String?)
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
if let data = json[Network.dataKey].arrayObject, let array = Mapper<T>().mapArray(JSONObject: data) {
observer.on(.next((array, json[Network.cursorKey].string)))
observer.on(.completed)
} else {
observer.on(.error(NSError.network(reason: ErrorMessage.json.rawValue, code: ErrorCode.json.rawValue)))
}
return Disposables.create()
}
}
}
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 {
observer.on(.error(NSError.network(reason: ErrorMessage.json.rawValue, code: ErrorCode.json.rawValue)))
}
return Disposables.create()
}
}
}
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 这个现代语言带来的优势。
作者:李富强投稿发布于本专栏
日常开发中,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表示业务处理成功,其他值表示错误码,那我们就可以在网络层判断 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)
}
)
.addDisposableTo(disposeBag)
看完了最终效果,我们开始架构我们的整个网络框架,首先是 Network 对象,用于管理网络请求的单例对象。下面这段代码解决了上面提到的网络层的第一个问题,通用参数的处理。
class Network {
static let `default`: Network = {
return Network()
}()
let baseUrl = "http://example.com"
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!
}
if let token = AccountCenter.default.user?.token {
newParameters["token"] = token
}
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") {
newParameters["app_version"] = version
}
return newParameters
}
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 方法的实现,我将会在代码注释中,为大家详细讲解这段代码:
protocol StringProtocol {}
extension String : StringProtocol {}
extension Observable where Element: StringProtocol {
func common() -> Observable<JSON> {
return self
.catchError({ (error) -> Observable<Element> in
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
return Observable<JSON>.create({ (observer) -> Disposable in
let json = JSON.parse(string)
if let code = json[Network.codeKey].int, code == Network.ok {
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)
}
}
经过上面的处理,我们处理了底层网络错误以及基本的业务数据的处理,这时我们获得了解析之后的 JSON 数据。但是一般来说,我们业务层更倾向于使用模型化之后的数据,在 OC 时代,这个模型化通常会比较麻烦,基本上所有的网络请求都需要把这个逻辑重复一遍。下面是我在 Swift 中的实现,同样,我会通过代码注释的方式为大家解释这段代码:
protocol JSONProtocol {}
extension JSON: JSONProtocol{}
extension Observable where Element: JSONProtocol {
typealias ListType<T> = ([T], String?)
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
if let data = json[Network.dataKey].arrayObject, let array = Mapper<T>().mapArray(JSONObject: data) {
observer.on(.next((array, json[Network.cursorKey].string)))
observer.on(.completed)
} else {
observer.on(.error(NSError.network(reason: ErrorMessage.json.rawValue, code: ErrorCode.json.rawValue)))
}
return Disposables.create()
}
}
}
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 {
observer.on(.error(NSError.network(reason: ErrorMessage.json.rawValue, code: ErrorCode.json.rawValue)))
}
return Disposables.create()
}
}
}
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 这个现代语言带来的优势。