作者:@请叫我汪二 授权本站转载。
最近在新项目中尝试使用 Moya+RxSwift+Argo 进行网络请求和解析,感觉还阔以,再来给大家安利一波。
Moya 是一个基于 Alamofire 的更高层网络请求封装,深入学习请参见官方文档:Moya/Docs。
使用 Moya 之后网络请求一般长了这样:
1
2
3
4
5
|
provider.request(.UserProfile("ashfurrow")) { (data, statusCode, response, error) in
if let data = data {
// do something with the data
}
}
|
Moya 提供了很多不错的特性,其中我感觉最棒的是 stub ,配合 sampleData 分分钟就完成了单元测试:
1
|
private let provider = MoyaProvider(stubClosure: MoyaProvider.ImmediatelyStub)
|
注意这里的 MoyaProvider.ImmediatelyStub ,我原以为它是个枚举类型,看了 MoyaProvider 定义发现这里应该传个closure ,看了 ImmediatelyStub 的定义发现原来它是个类方法:
1
2
3
4
5
6
7
8
9
|
public typealias StubClosure = Target -> Moya.StubBehavior
override public init(stubClosure: StubClosure = MoyaProvider.NeverStub, ...) {
}
public final class func ImmediatelyStub(_: Target) -> Moya.StubBehavior {
return .Immediate
}
|
如果想打印每次请求的参数,在组装 endpoint 的时候打印即可:
1
2
3
4
5
6
7
8
|
private func endpointMapping(target: Target) -> Endpoint {
if let parameters = target.parameters {
log.verbose("\(parameters)")
}
return MoyaProvider.DefaultEndpointMapping(target)
}
private let provider = RxMoyaProvider(endpointClosure: endpointMapping)
|
RxSwift 前面强行安利过两波,在此不再赘述啦,Moya 本身提供了 RxSwift 扩展,可以无缝衔接 RxSwift 和ReactiveCocoa ,于是打开方式变成了这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
private let provider = RxMoyaProvider()
private var disposeBag = DisposeBag()
extension ItemAPI {
static func getNewItems(completion: [Item] -> Void) {
disposeBag = DisposeBag()
provider
.request(.GetItems())
.subscribe(
onNext: { items in
completion(items)
}
)
.addDisposableTo(disposeBag)
}
}
|
Moya 的核心开发者、同时也是 Artsy 的成员:Ash Furrow, 在 AltConf 做过一次 《Functional Reactive Awesomeness With Swift》 的分享,推荐大家看一下,很可爱的!
Argo 是 thoughtbot 开源的函数式 JSON 解析转换库。说到 thoughtbot 就不得不提他司关于 JSON 解析质量很高的一系列文章:
Efficient JSON in Swift with Functional Concepts and Generics
Real World JSON Parsing with Swift
Parsing Embedded JSON and Arrays in Swift
Functional Swift for Dealing with Optional Values
Argo 基本上就是沿着这些文章的思路写出来的,相关的库还有 Runes 和 Curry。
使用 Argo 做 JSON 解析很有意思,大致长这样:
1
2
3
4
5
6
7
8
9
10
11
12
|
struct Item {
let id: String
let url: String
}
extension Item: Decodable {
static func decode(j: JSON) -> Decoded {
return curry(Item.init)
j <| "id"
j <| "url"
}
}
|
至于这其中各种符号的缘由,在几篇博客中都有讲解,还是挺有意思滴。
说完这三者,如何把它们串起来呢?Emergence 中的 Observable/Networking 给了我们答案。稍微整理后如下:
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
enum ORMError : ErrorType {
case ORMNoRepresentor
case ORMNotSuccessfulHTTP
case ORMNoData
case ORMCouldNotMakeObjectError
}
extension Observable {
private func resultFromJSON(object:[String: AnyObject], classType: T.Type) -> T? {
let decoded = classType.decode(JSON.parse(object))
switch decoded {
case .Success(let result):
return result as? T
case .Failure(let error):
log.error("\(error)")
return nil
}
}
func mapSuccessfulHTTPToObject(type: T.Type) -> Observable {
return map { representor in
guard let response = representor as? MoyaResponse else {
throw ORMError.ORMNoRepresentor
}
guard ((200...209) ~= response.statusCode) else {
if let json = try? NSJSONSerialization.JSONObjectWithData(response.data, options: .AllowFragments) as? [String: AnyObject] {
log.error("Got error message: \(json)")
}
throw ORMError.ORMNotSuccessfulHTTP
}
do {
guard let json = try NSJSONSerialization.JSONObjectWithData(response.data, options: .AllowFragments) as? [String: AnyObject] else {
throw ORMError.ORMCouldNotMakeObjectError
}
return self.resultFromJSON(json, classType:type)!
} catch {
throw ORMError.ORMCouldNotMakeObjectError
}
}
}
func mapSuccessfulHTTPToObjectArray(type: T.Type) -> Observable {
return map { response in
guard let response = response as? MoyaResponse else {
throw ORMError.ORMNoRepresentor
}
// Allow successful HTTP codes
guard ((200...209) ~= response.statusCode) else {
if let json = try? NSJSONSerialization.JSONObjectWithData(response.data, options: .AllowFragments) as? [String: AnyObject] {
log.error("Got error message: \(json)")
}
throw ORMError.ORMNotSuccessfulHTTP
}
do {
guard let json = try NSJSONSerialization.JSONObjectWithData(response.data, options: .AllowFragments) as? [[String : AnyObject]] else {
throw ORMError.ORMCouldNotMakeObjectError
}
// Objects are not guaranteed, thus cannot directly map.
var objects = [T]()
for dict in json {
if let obj = self.resultFromJSON(dict, classType:type) {
objects.append(obj)
}
}
return objects
} catch {
throw ORMError.ORMCouldNotMakeObjectError
}
}
}
}
|
这样在调用的时候就很舒服了,以前面的 Item 为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
private let provider = RxMoyaProvider()
private var disposeBag = DisposeBag()
extension ItemAPI {
static func getNewItems(records:[Record] = [], needCount: Int, completion: [Item] -> Void) {
disposeBag = DisposeBag()
provider
.request(.AddRecords(records, needCount))
.mapSuccessfulHTTPToObjectArray(Item)
.subscribe(
onNext: { items in
completion(items)
}
)
.addDisposableTo(disposeBag)
}
}
|
一个 mapSuccessfulHTTPToObjectArray 方法,直接将 JSON 字符串转换成了 Item 对象,并且传入了后面的数据流中,所以在 onNext 订阅的时候传入的就是 [Item] 数据,并且这个转换过程还是可以复用的,且适用于所有网络请求中 JSON 和 Model 的转换。爽就一个字,我只说一次。
爽!
匆匆读了一点 Emergence 和 Eidolon 的项目源码,没有深入不过已经受益匪浅。通过 bundle 管理 id 和 key 直接解决了我当初纠结已久的『完整项目开源如何优雅地保留 git 记录且保护项目隐私』的问题,还有 Moya/RxSwift 和Moya/ReactiveCocoa 这种子模块化处理也在共有模块管理这个问题上给了我一些启发。
真是很喜欢 Artsy 这样的团队,大家都一起做着自己喜欢的事情,还能站着把钱赚了。
所幸的是我也可以这样做自己喜欢的事情了,不过不赚钱。具体状况后面单独开一篇闲扯扯。
碎告。
参考资料:
RxSwift
Moya
Argo
Emergence
Eidolon
Efficient JSON in Swift with Functional Concepts and Generics
Real World JSON Parsing with Swift
Parsing Embedded JSON and Arrays in Swift
Functional Swift for Dealing with Optional Values