1、前言
说起来汗颜。
最近项目才开始使用 Swift 语言,正如我一个朋友嘲笑的:我们都快用烂的东西你们才开始用 ,我当时竟无言以对。
那既然用了 Swift,就要想办法用舒服,用明白。从 OC 工程转换到 Swift 工程,OC 的一些库,比如:网络请求库(AFNetworking),Json解析(YYModel), 响应式编程(RAC),还有网络请求的封装库(自己封装的或者第三方的) 就要按需更换了。
2、第三库的选择
1、网络请求库
毫无疑问是 Alamofire 了,就和 OC 项目选择 AFNetworking 一样。
2、Json 解析
Swift 也有不少,比如 SwiftyJSON,HandyJSON 等。
SwiftyJSON 非常强大,能帮助开发者将 Json 转成字典,按照 key 值取出时也帮助开发者进行路径判空,但是,我个人感觉用起来有点奇怪。
后来选择了阿里的 HandyJSON,HandyJSON 也支持结构体,支持将 Json 转成对象,支持模型数组,因为 Swift 上对泛型的支持,所以对比 OC 上的 YYModel 用起来更舒服些。
3、响应式编程
Swift 是静态语言,采用链式函数编程,Swift 中使用响应式编程,会让 Swift 更加简单和轻巧。
目前可以选择有很多,比如 ReactiveCocoa(Swift),RxSwift,Swift Combine(苹果自己的),各有优点缺点,各位客官可以自由比对选择,如果第一次接触的话,就自己随意选一个(毕竟使用过了才能对比)。
-
RxSwift 维护人员较多,这意味着你能轻易找到问题的解决方案,并且
RxSwift是ReactiveX的一个而已,它还有RxJava,RxPython等等。学会了一个,说不定其他都是一样哦。 -
ReactiveCocoa(Swift),这个是从 OC 上翻译过来的,有一些历史的 OC 包袱,但是原来熟悉 RAC 的会更容易上手。
-
Swift Combine是苹果自己的,自己的亲儿子,未来更新的几率会更大,并且不会出现第三库不在维护更新的。
4、网络库封装
如果你们公司 OC 项目上,有在网络库上再次封装的好用、强大的库,那么这个你就不用看了,你肯定只能混编。
对于之前自己项目只有简单再封装 AFNetworking 或者是新项目的,推荐使用 Moya。
Moya只是对 Alamofire 的再次封装,并不是网络请求库,所以使用 Moya就需要使用 Alamofire。
既然是网络库的再次封装,那么就可以将
Alamofire替换成其他的,只需要重写Moya+Alamofire.swift就可以了。我个人感觉一般没必要。
3、使用方法
Moya 是对 Alamofire 的再封装,如果只是使用的话,关心 Moya 的使用方法即可。
Moya 分别提供了Moya英文文档 和 Moya中文文档。(英文文档更全面)
1、熟悉 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"
// }
2、了解 Moya
上面只是初步使用了一下 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,其他会被自动赋上默认值。
1、endpointClosure
默认源码如下:
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 对象的时候可以任意改变参数,满足各种测试需求。
2、requestClosure
根据 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、stubClosure
这个参数提供了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. */
}
}
4、callbackQueue
回调线程。
5、manager
这里直接使用官方解释了,大多工程这里都用默认的。
接下来就是 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)
6、plugins
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))
}
}
当然,这只是一些代码片段,但是重要代码已经贴出来了,你可以以此为灵感继续扩展。
7、trackInflights
一个请求在 init 的时候将 trackInflights 设置为 true,那么在 Moya 中就会存储这个请求的 endpoint。在返回数据的时候,如果需要跟踪了重复请求,那么就将一次实际发送请求返回的数据,多次返回。
3、使用 Moya
3.1 和 3.2 基本上对 Moya 的使用详细说明了,这里就说调用方式吧。
1、普通调用方式
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)
}
}
2、RxSwift 调用方式
如果使用 RxSwift 需要导入库 RxMoya,根据 Moya 官方主页导入即可。
provider.rx.request(.createUser("三","张"))
.asObservable()
.mapJSON()
.mapHandyModel(type: UserModel.self)
.asSingle()
.subscribe { (userModel) in
} onFailure: { (error) in
} onDisposed: {
}
.disposable(by:disposable)
3、Moya 的二次封装
看完上面的内容,应该对 Moya 有一定的了解了,实际开发中,我们需要涉及的东西相当的多。比如,不同的接口可能需要不同的网络超时时间、还能可能需要配置接口需不需要对用户信息的验证,是否走本地测试数据,等等。
还有一些,比如 baseURL ,网络请求头 headers , HTTPMethod 大多都是一样的,如果每次都重新设置,那有一天改了 baseURL 的地址,headers 都需要增加一个参数,那时候杀人的心都有了。
1、扩展 TargetType 协议
既然 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 协议就可以了。
2、将 MoyaProvider 的创建封装
这里我就不写代码了,我推荐一个 GitHub 上的 Demo 看一下,本菜鸡也是从这里借鉴的。
4、使用 HandyJson
因为 HandyJson 可以支持结构体。Swift 中如果不需要继承的类,建议使用结构体,占用内存更小。
1、声明
声明一个 struct 或者 class,必须支持 HandyJSON 协议。
struct UserModel : HandyJSON {
var name : String?
var age : Int?
var address : String?
var hobby : [HobbyModel]? /// 支持模型数组,但是需要将数组中类型写清楚
}
2、使用
/// 普通模型转换
let parsedElement = UserModel.deserialize(from: AnyObject)
/// 数组模型转换
let parsedArray = [UserModel].deserialize(from: AnyObject)
3、联合 RxSwfit 使用
扩展 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)
5、RxSwift
关于 RxSwift 的使用方式看 Cooci 的博客 RxSwift 用法。
6、总结
有了这些,你就可以快速搭建新项目的网络请求了,如果感觉帮助了你些许,能给个赞最好了,感谢各位。