RxSwift
Rx
是微软出品的一个 Funtional Reactive Programming
框架,RxSwift
是它的一个 Swift 版本的实现。
RxSwift 的主要目的是能简单的处理多个异步操作的组合,和事件/数据流。
利用 RxSwift,我们可以把本来要分散写到各处的代码,通过方法链式调用来组合起来,非常的好看优雅。
举个例子,有如下操作:
点击按钮 -> 发送网络请求 -> 对返回的数据进行某种格式处理 -> 显示在一个 UILabel 上
代码如下:
1
2
3
4
5
6
7
|
sendRequestButton
.
rx_tap
.
flatMap
(
viewModel
.
loadData
)
.
throttle
(
0.3
,
scheduler
:
MainScheduler
.
instance
)
.
map
{
"\($0.debugDescription)"
}
.
bindTo
(
self
.
resultLabel
.
rx_text
)
.
addDisposableTo
(
disposeBag
)
|
是不是看上去很优雅呢?
另外这篇文章中也有一个类似的例子:
对应的代码是:
1
2
3
4
5
6
|
button
.
rx_tap
// 点击登录
.
flatMap
(
provider
.
login
)
// 登录请求
.
map
(
saveToken
)
// 保存 token
.
flatMap
(
provider
.
requestInfo
)
// 获取用户信息
.
subscribe
(
handleResult
)
// 处理结果
|
用一连串的链式调用就把一系列事件处理了,是不是很不错。
Moya
是 Artsy 团队的 Ash Furrow 主导开发的一个网络抽象层库。它在 Alamofire 基础上提供了一系列简单的抽象接口,让客户端代码不用去直接调用 Alamofire,也不用去关心 NSURLSession。同时提供了很多实用的功能。
它的 Target -> Endpoint -> Request
模式也使得每个请求都可以自由定制。
下面进入正题:
Moya 的 TargetType
协议规定的创建网络请求的方法,用枚举来创建,很有 Swift 的风格。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
enum
DataAPI
{
case
Data
}
extension
DataAPI
:
TargetType
{
var
baseURL
:
NSURL
{
return
NSURL
(
string
:
"http://localhost:3000"
)
!
}
var
path
:
String
{
return
"/data"
}
var
method
:
Moya
.
Method
{
return
.
GET
}
var
parameters
:
[
String
:
AnyObject
]
?
{
return
nil
}
var
sampleData
:
NSData
{
return
stubbedResponseFromJSONFile
(
"stub_data"
)
}
var
multipartBody
:
[
Moya
.
MultipartFormData
]
?
{
return
nil
}
}
|
数据模型的创建用了 SwiftyJSON
和 Moya_SwiftyJSONMapper
,方便将 JSON 直接映射成 Model 对象。
1
2
3
4
5
6
7
8
9
10
|
struct
DataModel
:
ALSwiftyJSONAble
{
var
title
:
String
?
var
content
:
String
?
init
?
(
jsonData
:
JSON
)
{
self
.
title
=
jsonData
[
"title"
]
.
string
self
.
content
=
jsonData
[
"content"
]
.
string
}
}
|
我们可使用 Moya 自带一个 RxSwift 的扩展来发送请求。
1
2
3
4
5
6
7
8
9
10
11
|
class
ViewModel
{
private
let
provider
=
RxMoyaProvider
(
)
// 创建为 RxSwift 扩展的 MoyaProvider
func
loadData
(
)
->
Observable
{
return
provider
.
request
(
.
DataRequest
)
// 通过某个 Target 来指定发送哪个请求
.
debug
(
)
// 打印请求发送中的调试信息
.
mapObject
(
DataModel
)
// 请求的结果映射为 DataModel 对象
}
}
|
然后在 ViewController 中就可以写上面说到过的那一段了
1
2
3
4
5
6
|
sendRequestButton
.
rx_tap
// 观察按钮点击信号
.
flatMap
(
viewModel
.
loadData
)
// 调用 loadData
.
map
{
"\($0.title) \($0.content)"
}
// 格式化显示内容
.
bindTo
(
self
.
resultLabel
.
rx_text
)
// 绑定到 UILabel 上
.
addDisposableTo
(
disposeBag
)
// 添加到 disposeBag,当 disposeBag 释放时,这个绑定关系也会被释放
|
这样就实现了 点击按钮 -> 发送网络请求 -> 显示结果
上面这一段没有考虑错误处理,这个后面会说。
URL 缓存则是采用 Alamofire 的缓存处理方式——用系统缓存(NSURLCache)。NSURLCache
默认采用的缓存策略是 NSURLRequestUseProtocolCachePolicy
。
缓存的具体方式可以由服务端在返回的响应头部添加 Cache-Control
字段来控制。
有一种缓存是系统的缓存做不到的,就是离线缓存。
离线缓存的流程是:
发请求前先看看本地有没有离线缓存
有 -> 使用离线缓存数据渲染界面 -> 发出网络请求 -> 用请求到的数据更新界面
无 -> 发出网络请求 -> 用请求到的数据更新界面
由于 Moya 没有提供离线缓存这个功能,只能自己写了。
为 RxMoyaProvider 扩展离线缓存功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
extension
RxMoyaProvider
{
func
tryUseOfflineCacheThenRequest
(
token
:
Target
)
->
Observable
{
return
Observable
.
create
{
[
weak
self
]
observer
->
Disposable
in
let
key
=
token
.
cacheKey
// 缓存 Key,可以根据自己的需求来写,这里采用的是 BaseURL + Path + Parameter转化为JSON字符串
// 先读取缓存内容,有则发出一个信号(onNext),没有则跳过
if
let
response
=
HSURLCache
.
sharedInstance
.
cachedResponseForKey
(
key
)
{
observer
.
onNext
(
response
)
}
// 发出真正的网络请求
let
cancelableToken
=
self
?
.
request
(
token
)
{
result
in
switch
result
{
case
let
.
Success
(
response
)
:
observer
.
onNext
(
response
)
observer
.
onCompleted
(
)
HSURLCache
.
sharedInstance
.
cacheResponse
(
response
,
forKey
:
key
)
case
let
.
Failure
(
error
)
:
observer
.
onError
(
error
)
}
}
return
AnonymousDisposable
{
cancelableToken
?
.
cancel
(
)
}
}
}
}
|
以上代码创建了一个信号序列,当有离线缓存时,会发出一个信号,当网络请求结果返回时,会发出一个信号,当网络请求失败时,也会发出一个错误信号。
1
2
3
|
上面的
HSURLCache
是我自己写的一个缓存类,通过
SQLite
把
Moya
的
Response
对象保存到数据库中。
由于
Moya
的
Response
对象是被
`
final
`
修饰的,无法通过继承方式为其添加
NSCoder
实现。所以就将
Response
的三个属性分别保存。
读缓存数据时也是读出三个属性的数据,再用他们创建成
Response
对象。
|
1
2
3
4
5
6
7
|
func
loadData
(
)
->
Observable
{
return
provider
.
tryUseOfflineCacheThenRequest
(
.
DataRequest
)
.
debug
(
)
.
distinctUntilChanged
(
)
.
mapObject
(
DataModel
)
}
|
使用离线缓存的网络请求方式可以写成这样,调用了上面所说的 tryUseOfflineCacheThenRequest
方法。
并且这里用了 RxSwift 的 distinctUntilChanged
方法,当两个信号完全一样时,会过滤掉后面的信号。这样避免页面在数据相同的情况下渲染两次。
可以通过判断 event 对象来处理错误,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
sendRequestButton
.
rx_tap
.
flatMap
(
viewModel
.
loadData
)
.
throttle
(
0.3
,
scheduler
:
MainScheduler
.
instance
)
.
map
{
"\($0.title) \($0.content)"
}
.
subscribe
{
event
in
switch
event
{
case
.
Next
(
let
data
)
:
print
(
data
)
case
.
Error
(
let
error
)
:
print
(
error
)
case
.
Completed
:
break
}
}
.
addDisposableTo
(
disposeBag
)
|
这时 Moya 的一个功能,可以在本地放置一个 json 文件,网络请求可以设置成读取本地文件内容来返回数据。可以在接口故障或为开发完时,客户端可以先用假数据来开发,先走通流程。
只要在创建 RxMoyaProvider 时指定一个参数 stubClosure
。
使用本地假数据:
1
|
RxMoyaProvider
(
stubClosure
:
MoyaProvider
.
ImmediatelyStub
)
|
使用网络接口真实数据:
1
|
RxMoyaProvider
(
stubClosure
:
MoyaProvider
.
NeverStub
)
|
Moya 也提供了一个模拟网络延迟的方法。
使用本地假数据并有 3 秒的延迟:
1
|
RxMoyaProvider
(
stubClosure
:
MoyaProvider
.
DelayedStub
(
3
)
)
|
例如如果想要在 Header 中添加一些字段,例如 access-token,可以通过 Moya 的 Endpoint Closure
方式实现,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
let
commonEndpointClosure
=
{
(
target
:
Target
)
->
Endpoint
in
var
URL
=
target
.
baseURL
.
URLByAppendingPathComponent
(
target
.
path
)
.
absoluteString
let
endpoint
=
Endpoint
(
URL
:
URL
,
sampleResponseClosure
:
{
.
NetworkResponse
(
200
,
target
.
sampleData
)
}
,
method
:
target
.
method
,
parameters
:
target
.
parameters
)
// 添加 AccessToken
if
let
accessToken
=
currentUser
.
accessToken
{
return
endpoint
.
endpointByAddingHTTPHeaderFields
(
[
"access-token"
:
accessToken
]
)
}
else
{
return
endpoint
}
}
|
另外 Moya 的插件机制也很好用,提供了两个接口,willSendRequest
和 didReceiveResponse
,可以在请求发出前和请求收到后做一些额外的处理,并且不和主功能耦合。
Moya 本身提供了打印网路请求日志的插件和 NetworkActivityIndicator 的插件。
例如检测 access-token 的合法性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
internal
final
class
AccessTokenPlugin
:
PluginType
{
func
willSendRequest
(
request
:
RequestType
,
target
:
TargetType
)
{
}
func
didReceiveResponse
(
result
:
Result
,
target
:
TargetType
)
{
switch
result
{
case
.
Success
(
let
response
)
:
do
{
let
jsonObject
=
try
response
.
mapJSON
(
)
let
json
=
JSON
(
jsonObject
)
if
json
[
"status"
]
.
intValue
==
InvalidStatus
{
NSNotificationCenter
.
defaultCenter
(
)
.
postNotificationName
(
"InvalidTokenNotification"
,
object
:
nil
)
}
}
catch
{
}
case
.
Failure
(
_
)
:
break
}
}
}
|
然后在创建 RxMoyaProvider 时注册插件:
1
|
private
let
provider
=
RxMoyaProvider
(
stubClosure
:
MoyaProvider
.
NeverStub
,
plugins
:
[
AccessTokenPlugin
(
)
]
)
|
对于用 Swift 编写的项目来说,可以有比 Objective-C 更优雅的方式来编写网络层代码。RxSwift + Moya 是个不错的选择,不仅能使代码更优雅美观,方便维护,还有具有一些很实用的小功能。