提及来汗颜。git
最近项目才开始使用 Swift
语言,正如我一个朋友嘲笑的:咱们都快用烂的东西大家才开始用 ,我当时竟无言以对。github
那既然用了 Swift
,就要想办法用舒服,用明白。从 OC
工程转换到 Swift
工程,OC 的一些库,好比:网络请求库(AFNetworking),Json解析(YYModel), 响应式编程(RAC),还有网络请求的封装库(本身封装的或者第三方的) 就要按需更换了。编程
毫无疑问是 Alamofire 了,就和 OC 项目选择 AFNetworking 同样。json
Swift
也有很多,好比 SwiftyJSON,HandyJSON 等。swift
SwiftyJSON
很是强大,能帮助开发者将 Json 转成字典,按照 key 值取出时也帮助开发者进行路径判空,可是,我我的感受用起来有点奇怪。api
后来选择了阿里的 HandyJSON,HandyJSON
也支持结构体,支持将 Json
转成对象,支持模型数组,由于 Swift
上对泛型的支持,因此对比 OC
上的 YYModel
用起来更舒服些。数组
Swift
是静态语言,采用链式函数编程,Swift
中使用响应式编程,会让 Swift
更加简单和轻巧。服务器
目前能够选择有不少,好比 ReactiveCocoa(Swift),RxSwift,Swift Combine(苹果本身的)
,各有优势缺点,各位客官能够自由比对选择,若是第一次接触的话,就本身随意选一个(毕竟使用过了才能对比)。markdown
RxSwift 维护人员较多,这意味着你能轻易找到问题的解决方案,而且 RxSwift
是 ReactiveX
的一个而已,它还有 RxJava
,RxPython
等等。学会了一个,说不定其余都是同样哦。网络
ReactiveCocoa(Swift),这个是从 OC 上翻译过来的,有一些历史的 OC 包袱,可是原来熟悉 RAC 的会更容易上手。
Swift Combine
是苹果本身的,本身的亲儿子,将来更新的概率会更大,而且不会出现第三库不在维护更新的。
若是大家公司 OC 项目上,有在网络库上再次封装的好用、强大的库,那么这个你就不用看了,你确定只能混编。
对于以前本身项目只有简单再封装 AFNetworking 或者是新项目的,推荐使用 Moya。
Moya
只是对 Alamofire
的再次封装,并非网络请求库,因此使用 Moya
就须要使用 Alamofire
。
既然是网络库的再次封装,那么就能够将
Alamofire
替换成其余的,只须要重写Moya+Alamofire.swift
就能够了。我我的感受通常不必。
Moya 是对 Alamofire
的再封装,若是只是使用的话,关心 Moya
的使用方法便可。
Moya 分别提供了Moya英文文档 和 Moya中文文档。(英文文档更全面)
下载官方的 Demo 后,先熟悉一下 Moya
的用法。
文档已经很详细,这里简单说明一下
/// 建立一个文件 MyService.swift
/// 声明一个枚举
enum MyService {
/// 分类放置你的请求调用函数
case createUser(firstName: String, lastName: String) } /// 扩展你的枚举,遵照 TargetType 协议 extension MyService: TargetType {
var baseURL: {
/// 放入 host
return baseURL;
}
var path: String {
case createUser(let firstName, let lastName) /// 返回具体请求路径 return "/user/create/user" } var method: Moya.Method {
switch self {
case .createUser:
/// 返回 .get 或者 .post
return .post;
}
}
var task: Task {
switch self {
case .createUser(let firstName, let lastName):
/// 具体请求参数
return .requestParameters(parameters: ["first_name": firstName, "last_name": lastName], encoding: JSONEncoding.default)
}
}
var sampleData: Data {
/// 若是服务器给了测试示例,能够放到这里
case .createUser(let firstName, let lastName):
return "{\"id\": 100, \"first_name\": \"\(firstName)\", \"last_name\": \"\(lastName)\"}".utf8Encoded
}
var headers: [String: String]? {
/// 请求头设置
return ["Content-type": "application/json"]
}
}
复制代码
而后你就能够在你的 ViewController 调用了:
let provider = MoyaProvider<MyService>()
provider.request(.createUser(firstName: "James", lastName: "Potter")) { result in
// do something with the result (read on for more details)
}
// The full request will result to the following:
// POST https://api.myservice.com/users
// Request body:
// {
// "first_name": "James",
// "last_name": "Potter"
// }
复制代码
上面只是初步使用了一下 Moya
,可是具体业务远比 Demo 复杂的多,Moya 也给咱们提供至关充足的施展空间。
第一步仍是建立一个文件,声明一个枚举,实现 TargetType
协议。可是建立 MoyaProvider
对象就不一样了。
上方代码只是使用了 let provider = MoyaProvider<MyService>()
建立,其实 MoyaProvider
中还有其余参数的。具体来看一下:
/// Initializes a provider.
public init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping, requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping, stubClosure: @escaping StubClosure = MoyaProvider.neverStub, callbackQueue: DispatchQueue? = nil, manager: Manager = MoyaProvider<Target>.defaultAlamofireManager(), plugins: [PluginType] = [], trackInflights: Bool = false) {
self.endpointClosure = endpointClosure
self.requestClosure = requestClosure
self.stubClosure = stubClosure
self.manager = manager
self.plugins = plugins
self.trackInflights = trackInflights
self.callbackQueue = callbackQueue
}
/// Returns an `Endpoint` based on the token, method, and parameters by invoking the `endpointClosure`.
open func endpoint(_ token: Target) -> Endpoint {
return endpointClosure(token)
}
复制代码
这里看到 MoyaProvider
对象 init
的时候还额外提供了 7 个参数,只是若是你使用了默认的 init
,其余会被自动赋上默认值。
默认源码以下:
final class func defaultEndpointMapping(for target: Target) -> Endpoint {
return Endpoint(
url: URL(target: target).absoluteString,
sampleResponseClosure: { .networkResponse(200, target.sampleData) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers
)
}
复制代码
这里是将建立的遵照协议的枚举 MyService
转化成 Endpoint
,每每咱们只是使用它的默认方法。 查阅 Endpoint
,发现还提供了两个方法:
open func adding(newHTTPHeaderFields: [String: String]) -> Endpoint
:用于更改请求头。
open func replacing(task: Task) -> Endpoint
: 将原有 MyService
枚举中实现的 task
进行替换。
可是有时候也有业务测试的需求,如:网络错误,超时等。就能够在这里实现。
Moya官方解释:因为它是一个闭包, 它将在每次调用API时被执行, 因此你能够作任何你想要的操做。
Moya
给了一个例子,只须要将对象 failureEndpointClosure
传入 MoyaProvider
的参数endpointClosure
便可。
let failureEndpointClosure = { (target: MyService) -> Endpoint in
let sampleResponseClosure = { () -> (EndpointSampleResponse) in
if shouldTimeout {
return .networkError(NSError())
} else {
return .networkResponse(200, target.sampleData)
}
}
return Endpoint(url: URL(target: target).absoluteString,
sampleResponseClosure: sampleResponseClosure,
method: target.method,
task: target.task)
}
复制代码
这里能够将 MyService
转化成 Endpoint
对象的时候能够任意改变参数,知足各类测试需求。
根据 Endpoint
生成 URLRequest
。
默认源码以下:
final class func defaultRequestMapping(for endpoint: Endpoint, closure: RequestResultClosure) {
do {
let urlRequest = try endpoint.urlRequest()
closure(.success(urlRequest))
} catch MoyaError.requestMapping(let url) {
closure(.failure(MoyaError.requestMapping(url)))
} catch MoyaError.parameterEncoding(let error) {
closure(.failure(MoyaError.parameterEncoding(error)))
} catch {
closure(.failure(MoyaError.underlying(error, nil)))
}
}
复制代码
代码中看到,经过 let urlRequest = try endpoint.urlRequest()
方式由 Endpoint
生成一个 URLRequest
对象,就意味着能够修改 URLRequest
中的参数,好比须要给 URLRequest
设置 timeoutInterval
等。
示例以下:
let requestClosure = { (endpoint: Endpoint, done: MoyaProvider.RequestResultClosure) in
do {
var request: URLRequest = try endpoint.urlRequest()
request.httpShouldHandleCookies = false
request.timeoutInterval = 15
done(.success(request))
} catch {
done(.failure(MoyaError.underlying(error, nil)))
}
}
复制代码
这个参数提供了3个枚举:
.never
(默认的):直接请求服务器;
.immediate
:走协议中 sampleData
示例数据;
.delayed(seconds)
能够把 stub 请求延迟指定时间,例如, .delayed(0.2)
能够把每一个 stub 请求延迟 0.2s 。 这个在单元测试中来模拟网络请求是很是有用的。
官方示例:
let stubClosure = { target: MyService -> Moya.StubBehavior in
switch target {
/* Return something different based on the target. */
}
}
复制代码
回调线程。
这里直接使用官方解释了,大多工程这里都用默认的。
接下来就是 session 参数,默认会得到一个经过基本配置进行初始化的自定义的 Alamofire.Session 实例对象
final class func defaultAlamofireSession() -> Session {
let configuration = URLSessionConfiguration.default
configuration.headers = .default
return Session(configuration: configuration, startRequestsImmediately: false)
}
复制代码
这儿只有一个须要注意的事情:因为在 AF 中建立一个 Alamofire.Request
对象时默认会当即触发请求,即便为单元测试进行 "stubbing" 请求也同样。 所以在Moya中, startRequestsImmediately
属性被默认设置成了 false
。
若是你须要自定义本身的 session , 好比说建立一个 SSL pinning
而且添加到 session
中,全部请求将经过自定义配置的 session
进行路由。
let serverTrustManager = ServerTrustManager(evaluators: ["example.com": PinnedCertificatesTrustEvaluator()])
let session = Session(
configuration: configuration,
startRequestsImmediately: false,
serverTrustManager: serverTrustManager
)
let provider = MoyaProvider<MyTarget>(session: session)
复制代码
plugins
是一个拦截器数组,能够传入多个遵照 PluginType
协议的对象。查阅 PluginType
协议:
/// - inject additional information into a request
public protocol PluginType {
/// Called to modify a request before sending.
/// requestClosure 生成 URLRequest 生成以后回调此方法
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest /// Called immediately before a request is sent over the network (or stubbed). /// 网络请求发出前回调此方法 func willSend(_ request: RequestType, target: TargetType) /// Called after a response has been received, but before the MoyaProvider has invoked its completion handler. /// 收到数据,Moya 尚未进行处理是回调此方法 func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) /// Called to modify a result before completion. /// 在网络 callBack 闭包回调前回调此方法 func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError> } 复制代码
这里能干的事情太多。
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest
方法回调后,能够将公共参数(版本号,token,userid)进行拼接,或者对数据进行 RSA
加密加签。举个 🌰 :
/// Called to modify a request before sending.
public func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
/// 这里作公共参数
let target = target as! MyService
var parameters : [String: Any]?
if let requstData = request.httpBody {
do {
let json = try JSONSerialization.jsonObject(with: requstData, options: .mutableContainers)
parameters = json as? [String: Any]
} catch {
/// 失败处理 ...
}
} else {
parameters = [String: Any]()
}
/// 拼接公共参数
parameters = paramsForPublicParmeters(parameters: parameters)
/// 加密加签
parameters = RSA.sign(withParamDic: parameters)
do {
/// 替换 httpBody
if let parameters = parameters {
return try request.encoded(parameters: parameters, parameterEncoding: JSONEncoding.default)
}
} catch {
/// 失败处理 ...
}
return request
}
复制代码
func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>
方法回调后,能够对数据进行验签解密。举个 🌰 :
/// Called to modify a result before completion.
public func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError> {
/// 验签
if case .success(let response) = result {
do {
let responseString = try response.mapJSON()
/// Json 转成 字典
let dic = JsonToDic(responseString)
/// 验签
if let _ = SignUntil.verifySign(withParamDic: dic) {
/// 数据解密
dic = RSA.decodeRSA(withParamDic: dic)
/// 从新生成 Moya.response
/// ...
/// 返回 Moya.response
return .success(response)
} else {
let error = NSError(domain: "验签失败", code: 1, userInfo: nil)
return .failure(MoyaError.underlying(error, nil))
}
} catch {
let error = NSError(domain: "拦截器 response 转 json 失败", code: 1, userInfo: nil)
return .failure(MoyaError.underlying(error, nil))
}
} else {
/// 本来就失败了就丢回了
return result
}
}
复制代码
willSend
和 didReceive
作日志打印:举个 🌰 :
/// 准备发送的时候拦截打印日志
public func willSend(_ request: RequestType, target: TargetType) {
/// 请求日志打印
NetWorkingLoggerOutPut.outPutLoggerRequest(request.request, andRequestURL: request.request?.url?.absoluteString)
}
/// 将要接受的时候拦截打印日志
public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
/// 返回日志打印
switch result {
case .success(let response):
NetWorkingLoggerOutPut.outPutLoggerReponseString(response.response, andRequest: response.request, andResponseObj:tryResponseToJSON(response: response) )
case .failure(let error):
NetWorkingLoggerOutPut.outPutLoggerReponseString(error.response?.response, andRequest: error.response?.request, andResponseObj: tryResponseToJSON(response: error.response))
}
}
复制代码
固然,这只是一些代码片断,可是重要代码已经贴出来了,你能够以此为灵感继续扩展。
一个请求在 init
的时候将 trackInflights
设置为 true
,那么在 Moya
中就会存储这个请求的 endpoint
。在返回数据的时候,若是须要跟踪了重复请求,那么就将一次实际发送请求返回的数据,屡次返回。
3.1 和 3.2 基本上对 Moya 的使用详细说明了,这里就说调用方式吧。
let provider = MoyaProvider(endpointClosure: endpointClosure,
requestClosure: requestClosure,
stubClosure: stubClosure,
manager: manager,
plugins: plugins)
provider.request(.createUser("三","张")) { result in
do {
let response = try result.get()
let value = try response.mapNSArray()
self.repos = value
} catch {
let printableError = error as CustomStringConvertible
self.showAlert("GitHub Fetch", message: printableError.description)
}
}
复制代码
若是使用 RxSwift
须要导入库 RxMoya
,根据 Moya
官方主页导入便可。
provider.rx.request(.createUser("三","张"))
.asObservable()
.mapJSON()
.mapHandyModel(type: UserModel.self)
.asSingle()
.subscribe { (userModel) in
} onFailure: { (error) in
} onDisposed: {
}
.disposable(by:disposable)
复制代码
看完上面的内容,应该对 Moya
有必定的了解了,实际开发中,咱们须要涉及的东西至关的多。好比,不一样的接口可能须要不一样的网络超时时间、还能可能须要配置接口需不须要对用户信息的验证,是否走本地测试数据,等等。
还有一些,好比 baseURL
,网络请求头 headers
, HTTPMethod
大多都是同样的,若是每次都从新设置,那有一天改了 baseURL
的地址,headers
都须要增长一个参数,那时候杀人的心都有了。
既然 Moya
已经提供了 TargetType
咱们何不扩展一下呢?
public protocol BaseHttpAPIManager: TargetType {
///是否验证用户身份
var validUser : Bool { get }
///超时时间
var timeoutInterval : Double { get }
/// 是否走测试数据 默认 .never
var stubBehavior: Moya.StubBehavior { get }
/// 等等 ...
}
复制代码
协议继承完成以后,这里就能够对咱们基本不变化的参数进行赋值。
extension BaseHttpAPIManager {
public var baseURL: URL {
return URL(string: WebService.shared.BaseURL)!
}
public var method: Moya.Method {
return .post
}
public var sampleData: Data {
return "response: test data".data(using: String.Encoding.utf8)!
}
public var task: Task {
return .requestPlain
}
///是否验证成功码
public var validationType: Moya.ValidationType {
return .successCodes
}
///请求头
public var headers: [String : String]? {
return WebService.shared.HttpHeaders
}
///如下为自定义扩展
public var validUser : Bool {
return WebService.shared.ValidUser
}
public var timeoutInterval : Double {
return WebService.shared.TimeoutInterval
}
/// 是否走测试数据 默认 .never
public var stubBehavior: StubBehavior {
return .never
}
//...
}
复制代码
由于 TargetType
协议是贯穿 Moya
整个核心的,因此你基本能够在任意地方使用它。以后只须要实现遵照 BaseHttpAPIManager
协议就能够了。
这里我就不写代码了,我推荐一个 GitHub 上的 Demo 看一下,本菜鸡也是从这里借鉴的。
由于 HandyJson 能够支持结构体。Swift
中若是不须要继承的类,建议使用结构体,占用内存更小。
声明一个 struct
或者 class
,必须支持 HandyJSON
协议。
struct UserModel : HandyJSON {
var name : String?
var age : Int?
var address : String?
var hobby : [HobbyModel]? /// 支持模型数组,可是须要将数组中类型写清楚
}
复制代码
/// 普通模型转换
let parsedElement = UserModel.deserialize(from: AnyObject)
/// 数组模型转换
let parsedArray = [UserModel].deserialize(from: AnyObject)
复制代码
扩展 Observable
就能够了。
public extension Observable where Element : Any {
/// 普通 Json 转 Model
func mapHandyModel <T : HandyJSON> (type : T.Type) -> Observable<T?> {
return self.map { (element) -> T? in
/// 这里的data 是 String 或者 dic
let data = element
let parsedElement : T?
if let string = data as? String {
parsedElement = T.deserialize(from: string)
} else if let dictionary = data as? Dictionary<String , Any> {
parsedElement = T.deserialize(from: dictionary)
} else if let dictionary = data as? [String : Any] {
parsedElement = T.deserialize(from: dictionary)
} else {
parsedElement = nil
}
return parsedElement
}
}
// 将 Json 转成 模型数组
func mapHandyModelArray<T: HandyJSON>(type: T.Type) -> Observable<[T?]?> {
return self.map { (element) -> [T?]? in
/// 这里的data 是 String 或者 dic
let data = element
let parsedArray : [T?]?
if let string = data as? String {
parsedArray = [T].deserialize(from: string)
} else if let array = data as? [Any] {
parsedArray = [T].deserialize(from: array)
} else {
parsedArray = nil
}
return parsedArray
}
}
}
复制代码
联合方式上方 3.3.2 Moya RxSwift 调用方式
已经给出了。
json.rx.mapHandyModel(type: UserModel.self)
.asSingle()
.subscribe { (userModel) in
} onFailure: { (error) in
} onDisposed: {
}
.disposable(by:disposable)
复制代码
关于 RxSwift
的使用方式看 Cooci 的博客 RxSwift 用法。
有了这些,你就能够快速搭建新项目的网络请求了,若是感受帮助了你些许,能给个赞最好了,感谢各位。