前言 上次罗列了一下在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)" } }
为了方便服务端获取更多的有关App的信息,单独写了一个类RequestCommonConfiguration。这是一个单例,里面为headers写死了一些默认的App的基本数据,类似Appversion、AppName等等。发送请求前,再对外面传进来的headers做了一次拼接。
根据timeout创建SessionManager
customManager是一个[TimeInterval:SessionManager]键值对,根据不同的timeout时间维护不同的SessionManager。这里为什么需要根据timeout来维护不同的session而不用一个全局的session,大概可以理解为如果公用一个session的话那当前一个请求还没有真正发出去的时候,下一个请求来了我们改变了session的timeout那么会影响到前一个请求。
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属性是将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) } }
这个方法先对传进来的json的success字段做一次校验,如果为false的话直接发送错误事件并上报。如果为ture的话,这里的returnKey这个参数应该是一个取服务端对应key里面的数据,目前默认约定好的应该是data(Key是一个结构体,里面有很多静态属性,data = “data”)里面的数据。这个最好应该是跟服务端约定好的事,可以不一定是data。目前看项目里面别的地方好像都没有传入这个参数,不过加一个这个参数可以保证灵活性吧,万一哪天一个新来的服务端大佬不熟悉这个规则,将data变为Data那还是有一定的补救的余地,不过这中情况也是服务端可以直接修复的,也不需要客户端来补救。
对应的,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的情况会找到当前App的topVC弹一个带有二维码的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:),刚看的时候可能会想HTTPMethod,observableData这些都是哪里来的,从没见过。嗯,别忘了它还遵守一个协议: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
还有url,headers,parameters这几个属性,可以留意一下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的转化,转化为model。Mapper方法是接入的第三方库ObjectMapper,这里就不展开讲了,下次有机会也对这个库做个总结。
结语 总体下来,这里对网络库的封装主要还是体现在对Rx的支持,这也更契合现在swift和RX结合使用的理念。还有就是对数据的校验和错误处理都比较严谨。这都是值得学习的地方。
这次SCCSwiftNetWork的总结就先到这里吧,有很多不到位的地方,也可能有很多理解或者是笔误的地方,请不吝赐教。谢谢!