zhou的博客.

SCCSwiftNetWork是怎么封装的

字数统计: 3.7k阅读时长: 17 min
2019/01/22 Share

前言

上次罗列了一下在App里面用到的一些网络请求方式,概括的说应该是两总类型:一类是直接url各种语法糖链接起来拿到请求数据,另一类就是针对RX做了一层封装,用RX的方式调用。那么这里我们就来总结一下那些类似"post""vaildData"之类的关键字里面做了什么,其他的方法又是怎么封装的。

从一个网络请求说起

先来看一个简单的网络请求吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let url = "https://www.baidu.com/" (这里是需要发送请求的url)

let params = [
"name":"tangeche",
"phone":"12312312",
"token":"YEHodfIOU",
"code": "123456"]

url
.headers(["Content-Type" : "application/json"])
.timeout(5)
.parameters(params)
.post(encoding: SCCParamterEncoding.jsonRaw)
.validData
.subscribe(onNext: { (json) in
print(json)
}, onError: { (error) in
print(error)
})
.disposed(by: bag)

这里的url根据类型推断应该是一个String类型,后面直接.headers,很明显是对String做了扩展。点进去看就知道,在Alamofire里面有对String做了一个扩展:

1
2
3
4
5
6
extension String: URLConvertible {
public func asURL() throws -> URL {
guard let url = URL(string: self) else { throw AFError.invalidURL(url: self) }
return url
}
}

然后,SCCSwiftNetWork里面又对URLConvertible做了一个扩展:

1
2
3
4
5
6
7
8
9
10
11
12
extension URLConvertible {

public func timeout(_ timeout: TimeInterval) -> SCCSwiftyNetwork.SCCURL

public func validatableParamters(_ params: SCCModelKeeper.SCCValidatableParameters) -> SCCSwiftyNetwork.SCCURL

public func parameters(_ params: Parameters) -> SCCSwiftyNetwork.SCCURL

public func headers(_ headers: HTTPHeaders) -> SCCSwiftyNetwork.SCCURL

public func download(to path: String) -> RxSwift.Observable<String>
}

这样我们就能直接拿一个String做上面那些操作了,比如timeout是设置网络超时时间,validatableParamters是对传入的参数做一些数据结构校验,也是大佬们封装好的,parameters显而易见是上传参数,headers请求头,download是一个将数据下载到本地的方法。但是,有没有注意到这些扩展方法里面都返回的是一个SCCURL,那这个SCCURL又是什么呢?

点开SCCURL类你会发现,原来他也是扩展自URLConvertible,那么他的实例照样可以.headers.parameters了。正如你看到的,前面这些都是在配置请求前的参数之类的,那么这些准备做好了是不是要开始发送网络请求了。没错,SCCURL里面就扩展了发送网络请求的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public extension SCCURL {

func get(encoding: ParameterEncoding = URLEncoding.default) -> Observable<(Any, String)> {
return request(method: .get, encoding: encoding)
}

func post(encoding: ParameterEncoding = URLEncoding.default) -> Observable<(Any, String)> {
return request(method: .post, encoding: encoding)
}

...

func request(method: HTTPMethod, encoding: ParameterEncoding) -> Observable<(Any, String)> {
return request(method: method, parameters: parameters, headers: headers, timeout: timeout, encoding: encoding)
}
...
}

这里的扩展应该涵盖了大部分的请求方式,主流的请求方式应该还是
get,post,正如例子中用到的post。最后,所有的请求都会归纳到方法,不妨把这个方法直接贴出来:

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
// Instance member cannot be uesed as default parameter
func request(method: HTTPMethod,
parameters: Parameters?,
headers: HTTPHeaders?,
timeout: TimeInterval? = nil,
encoding: ParameterEncoding = URLEncoding.default,
useCache: Bool = false) -> Observable<(Any, String)> {

var headers = headers
let url = (try? self.asURL())?.absoluteString ?? ""
return Observable<(Any, String)>.create { observer in
/// 一个判断是否有缓存的Bool值
var hasCache = false
/// 判断是否有缓存,如果有直接取缓存的数据返回
if useCache, let value = NSKeyedUnarchiver.unarchiveObject(withFile: self.cacheKey) {
observer.onNext((value, url))
hasCache = true
}

if var commonHeader = RequestCommonConfiguration.shared.headers {
/// 更新请求headers
headers?.forEach({ commonHeader[$0] = $1 })
headers = commonHeader
}

// Configure timeout for EscapedDog Dalao
/// 如果有设置超时时间,根据超时时间得到一个sessionManager
let manager: SessionManager
if let timeout = timeout, timeout > 0 {
if let cachedManager = customManagers[timeout] {
manager = cachedManager
} else {
manager = configureManager(with: timeout)
// Retain manager
customManagers[timeout] = manager
}
} else {
manager = defaultManager
}

let task = manager.request(self, method: method, parameters: parameters, encoding:encoding, headers: headers)
.responseJSON { response in
/// 如果抛出错误,上报错误
if let error = response.result.error {
SCCValidateResultReporter.request(url: url, errors: [], error: "\(error.code()) \(error.formatterDescription())")
observer.onError(error)
return
}

if let value = response.result.value {
/// 如果使用缓存的话,在之前没有缓存的情况下将数据写入缓存,有则不写
if useCache {
_ = NSKeyedArchiver.archiveRootObject(value, toFile: self.cacheKey)
if hasCache {
observer.onCompleted()
return
}
}
/// 如果不使用缓存,将返回的数据和url构成一个元组返回
observer.onNext((value, url))
}
observer.onCompleted()
}
return Disposables.create(with: task.cancel)
}
}

可以看到,这个方法发送了一个请求,返回了一个(Any, String)类型的的observable。说下几个值得注意的点:

  • 缓存处理

在传入参数中有个useCache(不过外部好像并没有用到过),如果使用缓存的话,那么会先根据url创建一个cacheKey尝试获取本地有没有缓存上次请求的数据,如果有的话,observer直接将得到的缓存发送出去,标记hasCache=true。在请求成功后,同样是使用缓存的情况下根据url创建的cacheKey将请求回来的数据写入缓存,已有缓存的话,observer发送一个completed事件。

1
2
3
4
5
6
7
8
9
private extension URLConvertible {
var cacheKey: String {
let url = try? asURL()
assert(url != nil, "Invalid URL")
let cachePath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0]
let fileName = url!.absoluteString.replacingOccurrences(of: "/", with: "-")
return "\(cachePath)/\(fileName)"
}
}
  • 默认headers

为了方便服务端获取更多的有关App的信息,单独写了一个类
RequestCommonConfiguration。这是一个单例,里面为
headers写死了一些默认的App的基本数据,类似Appversion
AppName等等。发送请求前,再对外面传进来的headers做了一次拼接。

  • 根据timeout创建SessionManager

customManager是一个[TimeInterval:SessionManager]键值对,根据不同的
timeout时间维护不同的SessionManager。这里为什么需要根据timeout来维护不同的session而不用一个全局的
session,大概可以理解为如果公用一个session的话那当前一个请求还没有真正发出去的时候,下一个请求来了我们改变了sessiontimeout那么会影响到前一个请求。

  • 错误上报
1
SCCValidateResultReporter.request(url: url, errors: [], error: "\(error.code()) \(error.formatterDescription())")

是一个封装好的将错误上报到一个错误统计平台,便于App的错误分析。

解析数据

请求发出去了,数据也拿到了,那么接下来就是解析数据了。前面我们已经知道网络请求回来返回的是一个(Any, String)类型的Observable,在数据解析文件里面对这个类型的Observable做这么几个方法的扩展:

1
2
3
4
5
6
7
8
9
10
11
public extension Observable where Element == (Any, String) {

var validDataUrl: Observable<(JSON, String)> {
return flatMap(parse)
}

var validData: Observable<JSON> {
return map{ $0.0 }
.flatMap(parseOnlyObject)
}
}
  • validDataUrl

可以看到这里的validDataUrl属性是将Observable<(Any,String)>转化为了Observable<(JSON,String)>,通过一个parse方法。

1
2
3
4
5
6
7
8
9
private func parse(object: (Any, String)) -> Observable<(JSON, String)> {
let json = JSON(object.0)
guard let _ = json.dictionary else {
let error = SCCError.invalidFormat(message: Message.invalidFormat, object: object.0)
SCCValidateResultReporter.request(url: object.1, errors: [], error: "\(error.code()) \(error.formatterDescription())")
return .error(error)
}
return handleJSON(json: json, url: object.1)
}

这里还看不出来对数据做了怎么的操作,只是将Any类型的数据转化为了JSON,如果有dictionary值则继续处理,否则发送一个错误事件并将错误上报。那我们再看看handleJSON这个方法是怎么实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private func handleJSON(returnKey: String = Key.data, json: JSON, url: String) -> Observable<(JSON, String)> {
/// 如果返回'success'字段为true
if check(json: json[Key.success]) {
if returnKey.isEmpty {
return .just((json, url))
}
return .just((json[returnKey], url))
} else {

let error = handleError(json: json)
SCCValidateResultReporter.request(url: url, errors: [], error: "\(error.code()) \(error.formatterDescription())")
return .error(error)
}
}

这个方法先对传进来的jsonsuccess字段做一次校验,如果为false的话直接发送错误事件并上报。如果为ture的话,这里的returnKey这个参数应该是一个取服务端对应key里面的数据,目前默认约定好的应该是data(Key是一个结构体,里面有很多静态属性,data = “data”)里面的数据。这个最好应该是跟服务端约定好的事,可以不一定是data。目前看项目里面别的地方好像都没有传入这个参数,不过加一个这个参数可以保证灵活性吧,万一哪天一个新来的服务端大佬不熟悉这个规则,将data变为Data那还是有一定的补救的余地,不过这中情况也是服务端可以直接修复的,也不需要客户端来补救。

  • validData

对应的,validData是将Observable<(Any,String)>转化为Observable<JSON>,这里先用map(Any,String)转化为Any,再flatMap调用parseOnlyObject

1
2
3
4
5
6
7
private func parseOnlyObject(object: Any) -> Observable<JSON> {
let json = JSON(object)
guard let _ = json.dictionary else {
return .error(SCCError.invalidFormat(message: Message.invalidFormat, object: object))
}
return handleJSON(json: json)
}

这里和上面和类似,就不用过多笔墨了。可以看一下handleError这个方法怎么实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func handleError(json: JSON) -> SCCError {

let code = json[Key.code].intValue
if ErrorCode.notLogin.rawValue == code || ErrorCode.tokenLost.rawValue == code {
NotificationCenter.default.post(name: .SCCNotLogin, object: nil)
}
let traceId = json[Key.traceId].stringValue
var message = "\(json[Key.msg].stringValue)\n\(traceId)"
if !traceId.isEmpty, let topVC = UIViewController.applicationTopVC() {
SCCModulor.sharedInstance().moduleName("wirelessToast", openWithParams: ["icon": "qrcode", "text": message, "qrcodeText": traceId, "vc": topVC, "duration": "2000"], callback:nil)
//已经弹过toast,防止上层业务二次弹出,将message置空,ui toast层做判断拦截
message = ""
}
let error = SCCError.business(errorCode: code,
message: message,
object: json[Key.data])
return error
}

SCCError是一个对App网络错误类别统一归类的的类,这里的业务逻辑是如果是未登录或者token丢失的错误的话会发送一个未登录通知。另一个逻辑是在有traceId的情况会找到当前ApptopVC弹一个带有二维码的toast。这个找topVc的方法值得学习下:

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
extension UIViewController {

static func applicationTopVC() -> UIViewController? {
var window: UIWindow? = UIApplication.shared.keyWindow
if window?.windowLevel != UIWindowLevelNormal {
let windows = UIApplication.shared.windows
for tmpWin: UIWindow in windows {
if tmpWin.windowLevel == UIWindowLevelNormal {
window = tmpWin
break
}
}
}
for frontView: UIView? in window?.subviews ?? [UIView?]() {
var nextResponder = frontView?.next
if let lenderClass = objc_getClass("UILayoutContainerView") as? UIView {

if let isMember = frontView?.isMember(of: lenderClass.classForCoder), !isMember {
let arr = frontView?.value(forKey: "subviewCache") as? [Any]
if (arr?.count ?? 0) > 0 {
let v = arr?[0] as? UIView
nextResponder = v?.next
} else {
nextResponder = frontView?.subviews[0].next
}
}
}

if (nextResponder is UITabBarController) {
let tabbar = nextResponder as? UITabBarController
if let selectedIndex = tabbar?.selectedIndex {
let vc = tabbar?.viewControllers?[selectedIndex]
guard let nav = vc as? UINavigationController else {
return vc
}
return nav.childViewControllers.last
}
} else if (nextResponder is UINavigationController) {
let nav = nextResponder as? UINavigationController
return nav?.viewControllers.last
} else if (nextResponder is UIViewController) {
let vc = nextResponder as? UIViewController
return vc
}
}
return nil
}
}

到此,第一类请求方式差不多就过完了,接下来我们主要看看怎么对这些方法进行RX方法的封装。

API+RX

跟第一类方式一样,先看一个简单的网络请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
BankResource()
.rx.jsonWithParams()
.subscribe({ (event) in
switch event {
case .next(_):
break
case .error(let error):
self.scc.toast(content: error.formatterDescription())
break
default: break
}
})
.disposed(by: bag)

分析这个方法之前我们先看一个类SCCApi

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
open class SCCApi: NSObject, APIType {

public lazy var url: SCCURL = {
return SCCURL(url: self.apiURL())
}()

public lazy var method: HTTPMethod = {
return self.apiType()
}()

open func apiURL() -> String! {
assertionFailure("Should be overrided by subclass.")
return ""
}

open func apiType() -> HTTPMethod {
return .get
}

open func composeJSONWithPropertyParams() -> Observable<JSON> {
return observableData.json
}

open func composeJSONWithParams(_ params: Parameters = [:]) -> Observable<JSON> {
return url.request(method: method, parameters: params, headers: headers).json
}
}

postfix operator =>

public postfix func => <T: Mappable>(object: Any?) -> T? {

return Mapper().map(JSONObject: object)
}

public postfix func => <T: Mappable>(object: Any?) -> [T]? {

return Mapper().mapArray(JSONObject: object)
}

可以看到,这个类有两个属性:url,method;四个方法:apiURL()apiType()composeJSONWithPropertyParams()composeJSONWithParams(params:),刚看的时候可能会想HTTPMethodobservableData这些都是哪里来的,从没见过。嗯,别忘了它还遵守一个协议:APIType,我们看看里面都有些什么:

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
public protocol APIType {
var url: SCCURL { get }
var method: HTTPMethod { get }
var headers: HTTPHeaders { get }
var parameters: Parameters { get }
var observableData: Observable<(Any, String)> { get }
}

public extension APIType {

var method: HTTPMethod { return .get }
var headers: HTTPHeaders {
return RequestCommonConfiguration.shared.headers ?? [:]
}

var parameters: Parameters {
var parameters: Parameters = [:]
let mirror = Mirror(reflecting: self)
mirror.children.forEach { parameters[$0.label!] = $0.value }
return parameters
}

var observableData: Observable<(Any, String)> {
return url.request(method: method, parameters: parameters, headers: headers)
}
}

我们刚才疑惑的observableData在这里好像找到了,他是一个Observable<(Any,String)>,在扩展里面默认是根据默认参数调用了之前提到的网络请求方法。而HTTPMethod是一个Alamofire请求方式的类型别名:

1
2
3
public typealias Parameters = Alamofire.Parameters
public typealias HTTPHeaders = Alamofire.HTTPHeaders
public typealias HTTPMethod = Alamofire.HTTPMethod

还有urlheadersparameters这几个属性,可以留意一下parameters这个属性,它里面有用到Mirror(反射),这个很有意思。Mirror(reflecting: self)能够反射出这个实例的类型(subjectType)、属性集合(children)、对象展示类型(displayStyle)。这和OC里面runtime很类似,我们可以动态获取一个实例的属性了,正如上面代码里的一样。再倒回去看SCCApi,后面两个比较长的方法也就是将请求回来的数据做一次JSON(这里的json方法是老方法,应该用vaildData)解析,然后返回一个Observable<JSON>.这里有一个新版本的SCCApi:SCCStandarApi:

1
2
3
4
5
6
7
8
9
10
11
/// For compatibility with older version
open class SCCStandardApi: SCCApi {

open override func composeJSONWithPropertyParams() -> Observable<JSON> {
return observableData.validData
}

open override func composeJSONWithParams(_ params: Parameters) -> Observable<JSON> {
return url.request(method: method, parameters: params, headers: headers).validData
}
}

最后终于到了RX封装的方法了:

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
extension Reactive where Base: SCCApi {

public func jsonWithPropertyParams() -> Observable<JSON> {
return self.base.composeJSONWithPropertyParams()
}

public func jsonWithParams(_ params: Parameters = [:]) -> Observable<JSON> {
return self.base.composeJSONWithParams(params)
}
}

extension Reactive where Base: SCCApi {

public func modelWithPropertyParams<T: Mappable>(_ modelType: T.Type, key: String? = nil) -> Observable<T?> {
return base
.rx
.jsonWithPropertyParams()
.map(valueForKey(key))
.map(=>)
}

public func modelWithParams<T: Mappable>(_ params: Parameters = [:], modelType: T.Type, key: String? = nil) -> Observable<T?> {
return base
.rx
.jsonWithParams(params)
.map(valueForKey(key))
.map(=>)
}

public func modelsWithPropertyParams<T: Mappable>(_ modelType: T.Type, key: String? = nil) -> Observable<[T]?> {
return base
.rx
.jsonWithPropertyParams()
.map(valueForKey(key))
.map(=>)
}

public func modelsWithParams<T: Mappable>(_ params: Parameters = [:], modelType: T.Type, key: String? = nil) -> Observable<[T]?> {
return base
.rx
.jsonWithParams(params)
.map(valueForKey(key))
.map(=>)
}
}

这些方法分为两组,一组是返回Observable<JSON>,一类是返回一个泛型model。第一个方法不带参数,获取api的属性作为参数进行网络请求。第二个方法需要传入参数。
下面一组方法也是在将数据转为JSON之后再通过一层Mapper的转化,转化为modelMapper方法是接入的第三方库ObjectMapper,这里就不展开讲了,下次有机会也对这个库做个总结。

结语

总体下来,这里对网络库的封装主要还是体现在对Rx的支持,这也更契合现在swiftRX结合使用的理念。还有就是对数据的校验和错误处理都比较严谨。这都是值得学习的地方。

这次SCCSwiftNetWork的总结就先到这里吧,有很多不到位的地方,也可能有很多理解或者是笔误的地方,请不吝赐教。谢谢!

CATALOG
  1. 1. 前言
  2. 2. 从一个网络请求说起
  3. 3. 解析数据
  4. 4. API+RX
  5. 5. 结语