Moya + Alamofire + HandyJson + RxSwift 搭建一个新项目的网络请求

一、前言

提及来汗颜。git

最近项目才开始使用 Swift 语言,正如我一个朋友嘲笑的:咱们都快用烂的东西大家才开始用 ,我当时竟无言以对。github

那既然用了 Swift,就要想办法用舒服,用明白。从 OC 工程转换到 Swift 工程,OC 的一些库,好比:网络请求库(AFNetworking),Json解析(YYModel), 响应式编程(RAC),还有网络请求的封装库(本身封装的或者第三方的) 就要按需更换了。编程

二、第三库的选择

一、网络请求库

毫无疑问是 Alamofire 了,就和 OC 项目选择 AFNetworking 同样。json

二、Json 解析

Swift 也有很多,好比 SwiftyJSONHandyJSON 等。swift

SwiftyJSON 很是强大,能帮助开发者将 Json 转成字典,按照 key 值取出时也帮助开发者进行路径判空,可是,我我的感受用起来有点奇怪。api

后来选择了阿里的 HandyJSONHandyJSON 也支持结构体,支持将 Json 转成对象,支持模型数组,由于 Swift 上对泛型的支持,因此对比 OC 上的 YYModel 用起来更舒服些。数组

三、响应式编程

Swift 是静态语言,采用链式函数编程,Swift 中使用响应式编程,会让 Swift 更加简单和轻巧。服务器

目前能够选择有不少,好比 ReactiveCocoa(Swift)RxSwiftSwift Combine(苹果本身的),各有优势缺点,各位客官能够自由比对选择,若是第一次接触的话,就本身随意选一个(毕竟使用过了才能对比)。markdown

  • RxSwift 维护人员较多,这意味着你能轻易找到问题的解决方案,而且 RxSwiftReactiveX 的一个而已,它还有 RxJavaRxPython 等等。学会了一个,说不定其余都是同样哦。网络

  • ReactiveCocoa(Swift),这个是从 OC 上翻译过来的,有一些历史的 OC 包袱,可是原来熟悉 RAC 的会更容易上手。

  • Swift Combine 是苹果本身的,本身的亲儿子,将来更新的概率会更大,而且不会出现第三库不在维护更新的。

四、网络库封装

若是大家公司 OC 项目上,有在网络库上再次封装的好用、强大的库,那么这个你就不用看了,你确定只能混编。

对于以前本身项目只有简单再封装 AFNetworking 或者是新项目的,推荐使用 Moya

Moya只是对 Alamofire 的再次封装,并非网络请求库,因此使用 Moya就须要使用 Alamofire

既然是网络库的再次封装,那么就能够将 Alamofire 替换成其余的,只须要重写 Moya+Alamofire.swift 就能够了。我我的感受通常不必。

三、使用方法

Moya 是对 Alamofire 的再封装,若是只是使用的话,关心 Moya 的使用方法便可。

Moya 分别提供了Moya英文文档Moya中文文档。(英文文档更全面)

一、熟悉 Moya

image.png

下载官方的 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

上面只是初步使用了一下 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,其余会被自动赋上默认值。

一、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 对象的时候能够任意改变参数,知足各类测试需求。

二、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)))
    }
}
复制代码
三、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. */
    }
}
复制代码
四、callbackQueue

回调线程。

五、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)
复制代码
六、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
    }
}
复制代码
  • 你还能够在 willSenddidReceive 作日志打印:

举个 🌰 :

/// 准备发送的时候拦截打印日志
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))
    }
}
复制代码

固然,这只是一些代码片断,可是重要代码已经贴出来了,你能够以此为灵感继续扩展。

七、trackInflights

一个请求在 init 的时候将 trackInflights 设置为 true,那么在 Moya 中就会存储这个请求的 endpoint。在返回数据的时候,若是须要跟踪了重复请求,那么就将一次实际发送请求返回的数据,屡次返回。

三、使用 Moya

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 调用方式

若是使用 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 的二次封装

看完上面的内容,应该对 Moya 有必定的了解了,实际开发中,咱们须要涉及的东西至关的多。好比,不一样的接口可能须要不一样的网络超时时间、还能可能须要配置接口需不须要对用户信息的验证,是否走本地测试数据,等等。

还有一些,好比 baseURL ,网络请求头 headersHTTPMethod 大多都是同样的,若是每次都从新设置,那有一天改了 baseURL 的地址,headers 都须要增长一个参数,那时候杀人的心都有了。

一、扩展 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 协议就能够了。

二、将 MoyaProvider 的建立封装

这里我就不写代码了,我推荐一个 GitHub 上的 Demo 看一下,本菜鸡也是从这里借鉴的。

四、使用 HandyJson

由于 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)

复制代码

三、联合 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)
复制代码

五、RxSwift

关于 RxSwift 的使用方式看 Cooci 的博客 RxSwift 用法

六、总结

有了这些,你就能够快速搭建新项目的网络请求了,若是感受帮助了你些许,能给个赞最好了,感谢各位。

相关文章
相关标签/搜索