阅读 3839

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

1、前言

说起来汗颜。

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

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

2、第三库的选择

1、网络请求库

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

2、Json 解析

Swift 也有不少,比如 SwiftyJSONHandyJSON 等。

SwiftyJSON 非常强大,能帮助开发者将 Json 转成字典,按照 key 值取出时也帮助开发者进行路径判空,但是,我个人感觉用起来有点奇怪。

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

3、响应式编程

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

目前可以选择有很多,比如 ReactiveCocoa(Swift)RxSwiftSwift Combine(苹果自己的),各有优点缺点,各位客官可以自由比对选择,如果第一次接触的话,就自己随意选一个(毕竟使用过了才能对比)。

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

  • 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

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"
// }
复制代码

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
    }
}
复制代码
  • 你还可以在 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))
    }
}
复制代码

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

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 ,网络请求头 headersHTTPMethod 大多都是一样的,如果每次都重新设置,那有一天改了 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、总结

有了这些,你就可以快速搭建新项目的网络请求了,如果感觉帮助了你些许,能给个赞最好了,感谢各位。

文章分类
iOS
文章标签