美文网首页iOS日常开发
Moya封装实践 2023-05-13 周六

Moya封装实践 2023-05-13 周六

作者: 勇往直前888 | 来源:发表于2023-05-14 14:17 被阅读0次

简介

从OC切换到Swift,还真的不习惯。网络层同事已经封装了Alamofire,能用,但是总感觉不爽。看到网上都推荐使用Moya,所以也试着封装了一下。实际尝试下来,确实比直接使用Alamofire方便很多,推荐使用Moya。

设计模式

感觉跟大话设计模式中提到的命令模式很像,书中用了一个烤肉店的例子来类比,其结构图如下:


image.png

命令(烤肉菜单)

  • 这个对应的就是TargetType,这是一个协议protocol,并不是基类。不过这个没有关系,Swift是面向协议的编程。

  • 一般都用一个枚举来实现这个协议,非常切合“命令”这个语境,烤肉店例子里,就是“菜单”

  • 枚举值携带的变量,要么没有,要么统一为字典,具体的参数字段让上层调用者区分,这里只是统一传递。

  • baseUrl和header之类的一般是公共的,可以放到一个辅助的常量定义struct中,这里是MoyaConfig

  • Get请求,参数的编码格式选择URLEncoding.default,这个就相当于在url之后拼接?key=value,带来很大的便利。

  • 数据请求,Post请求占大多数,所以提供默认的编码格式JSONEncoding.default,只是这里没有Post例子,而XCode显示警告,暂时注释掉而已。

import Foundation
import Moya

/// 枚举值,参数统一为字典,有可能为空
enum MoyaRequestCommand {
    /// resource模块
    case resourceNoticeDetail([String: Any])
    case resourceAdvertPage([String: Any])
}

extension MoyaRequestCommand: TargetType {
    var baseURL: URL {
        MoyaConfig.baseURL
    }

    var path: String {
        switch self {
        case .resourceNoticeDetail:
            return "/gateway/resource/notice/detail"
        case .resourceAdvertPage:
            return "/gateway/resource/advert/page"
        }
    }

    var method: Moya.Method {
        switch self {
        case .resourceNoticeDetail:
            return .get
        case .resourceAdvertPage:
            return .get
        }
    }

    var task: Moya.Task {
        /// 默认的公共参数,采用默认的编码
//        var defaultParams: [String: Any] = [:]

        switch self {
        case .resourceNoticeDetail(let params):
            /// Get请求需要这个,其实就是在url后面拼接参数
            return .requestParameters(parameters: params, encoding: URLEncoding.default)
        case .resourceAdvertPage(let params):
            /// Get请求需要这个,其实就是在url后面拼接参数
            return .requestParameters(parameters: params, encoding: URLEncoding.default)

//        default: break
        }

        /// 默认参数编码
//        return .requestParameters(parameters: defaultParams, encoding: JSONEncoding.default)
    }

    var headers: [String: String]? {
        MoyaConfig.headers
    }
}

执行者(烤肉串者)

  • 这里的对应角色是MoyaProvider

结果处理

  • Moya的返回结果可以简单地理解为下面的枚举(简化过的):
enum Result {
    case success(Data)
    case failure(Error)
}
  • 这里的成功失败是网络访问这个过程,一般是状态为200~299;成功返回数据Data,失败返回错误Error。

  • Moya提供mapJSON方法将Data转化为字典,这个可能抛出异常,很方便实用;Error的时候,我们一般需要知道一下错误原因error.errorDescription

  • 除了网络访问本身的错误,还有接口定义的逻辑错误。比如我们用的就非常直接,统一返回一个字典,Code字段肯定有,200表示成功,其他都是错误。错误的时候,错误原因放在msg字段中。成功的时候,数据放在Data字段中。所以,根据这种设定,给出了对应的Model

/// 网络返回字段
struct MoyaNetworkDataModel: HandyJSON {
    var code: Int = 0
    var msg: String?
    var data: Any?
}
  • Moya可以把Data转化为字典[String:Any];把字典转化为Model,就需要HandyJSON

Log和Loading

  • 网络Log非常重要,在调试的时候很有帮助。Moya提供了现成的插件NetworkLoggerPlugin,可以直接使用,非常方便。

  • 网络Loading也有需要,Moya只提供了执行时机,具体Loading视图需要自己提供,比如PKHUD

  • 这里要注意的是,Loading是UI,要求主线程运行,而NetworkActivityPlugin运行在后台进程,需要切换一下。

  • 另外,Loading是大多数接口需要的,但是也有很多接口不应该显示。NetworkActivityPlugin提供了TargetType参数,在这里可以根据具体的命令进行特殊处理(直接返回,不显示Loading)。

    let provider = MoyaProvider<MoyaRequestCommand>(plugins: [NetworkLoggerPlugin(), NetworkActivityPlugin(networkActivityClosure: { change, target in
        
        /// 不需要loading的命令列在这里
        switch target {
        case MoyaRequestCommand.resourceAdvertPage:
            return;
        default: break
        }
        
        /// 添加loading
        switch change {
        case .began:
            OperationQueue.main.addOperation {
                HudUtil.show()
            }
        case .ended:
            OperationQueue.main.addOperation {
                HudUtil.hide()
            }
        }
    })])

数据回传方式

  • 不论是Alamofire,还是Moya,最终的数据都是以回调的方式返回的。能否改成async 函数?
    /// async函数形式的接口
    func asyncRequest(command target: MoyaRequestCommand, isShowToast: Bool = true) async -> MoyaNetworkDataModel {
        /// 将回调改为async函数
        await withCheckedContinuation { continuation in
            doRequset(command: target, isShowToast: isShowToast) { model in
                continuation.resume(returning: model)
            }
        }
    }

封装形式

  • 在OC的时候,毫无疑问,用class;但是Swift时代,struct也可以啊。选哪个呢?其实不用纠结,两者都行。比如函数式编程,就偏向用struct,当然我不是啊。

  • 网络访问,MoyaProvider本身的语义,从命令模式中执行者(烤肉串者)的语义来说,这里适合用单例;而单例,一般用class

  • 单例一般用类,并且一般命名上以XXXManager的形式

import Foundation
import Moya

class MoyaNetworkManager {
    /// 单例实例
    /// MoyaNetworkManager.sharedInstance.就是单例的用法
    static let sharedInstance = MoyaNetworkManager()
    
    /// async函数形式的接口
    func asyncRequest(command target: MoyaRequestCommand, isShowToast: Bool = true) async -> MoyaNetworkDataModel {
        /// 将回调改为async函数
        await withCheckedContinuation { continuation in
            doRequset(command: target, isShowToast: isShowToast) { model in
                continuation.resume(returning: model)
            }
        }
    }
    
    /// 网络执行者,禁止外部直接访问
    private let provider = MoyaProvider<MoyaRequestCommand>(plugins: [NetworkLoggerPlugin(), NetworkActivityPlugin(networkActivityClosure: { change, target in
        
        /// 不需要loading的命令列在这里
        switch target {
        case MoyaRequestCommand.resourceAdvertPage:
            return;
        default: break
        }
        
        /// 添加loading
        switch change {
        case .began:
            OperationQueue.main.addOperation {
                HudUtil.show()
            }
        case .ended:
            OperationQueue.main.addOperation {
                HudUtil.hide()
            }
        }
    })])
    
    /// 请求完成的回调
    typealias NetworkCompletion = (MoyaNetworkDataModel) -> Void
    
    /// 统一调用provider完成网络请求;统一处理错误:通常是toast以下
    /// 回调形式的数据返回,不推荐使用,这里设置为私有
    private func doRequset(command target: MoyaRequestCommand, isShowToast: Bool = true, completion: @escaping NetworkCompletion) {
        provider.request(target) { result in
            switch result {
            case .success(let response):
                do {
                    guard let json = try response.mapJSON() as? [String: Any] else {
                        let parseError = MoyaNetworkDataModel(code: -1, msg: "服务器返回的不是JSON数据")
                        if isShowToast {
                            ToastUtil.show(parseError.msg)
                        }
                        completion(parseError)
                        return
                    }
                    
                    guard let model = MoyaNetworkDataModel.deserialize(from: json) else {
                        let modelError = MoyaNetworkDataModel(code: -2, msg: "JSON数据转MoyaNetworkDataModel失败")
                        if isShowToast {
                            ToastUtil.show(modelError.msg)
                        }
                        completion(modelError)
                        return
                    }
                    
                    /// 判断逻辑问题;统一处理,通常是toast一下
                    if model.code != 200 {
                        if isShowToast {
                            ToastUtil.show(model.msg)
                        }
                    }
                    
                    /// 成功返回
                    completion(model)
                } catch {
                    let catchError = MoyaNetworkDataModel(code: -3, msg: "解析出错:\(error.localizedDescription)")
                    if isShowToast {
                        ToastUtil.show(catchError.msg)
                    }
                    completion(catchError)
                }
                
            case .failure(let error):
                let networkError = MoyaNetworkDataModel(code: -4, msg: "请求失败:\(String(describing: error.errorDescription))")
                if isShowToast {
                    ToastUtil.show(networkError.msg)
                }
                completion(networkError)
            }
        }
    }
}

调用者(服务员)

  • 直接调用单例还是很麻烦的,那个sharedInstance很让人讨厌;所以,一般都会再封装一层,方便使用。

  • 从命令模式将,调用者(invoke)(服务员)也是很有必要的。命令(烤肉菜单)只是传递参数,并不需要知道具体参数的含义。而调用者(invoke)(服务员)能承担解释命令含义的责任。

  • 这个没有单例的含义。就像烤肉店如果生意好,就会多招几个服务员,按职责分类,能够更好的服务不同类别的客户。所以这里就选择了struct

/// Resource模块接口封装
struct ResourceApi {
    /// 根据id获取广告详情页内容
    static func asyncNoticeDetail(id: String?) async -> Any? {
        guard let id = id else {
            return nil;
        }
        let model = await MoyaNetworkManager.sharedInstance.asyncRequest(command: .resourceNoticeDetail(["id": id]))
        return model.data
    }
    
    /// 根据id获取广告详情页内容
    static func asyncAdvertPage(_ page: String?) async -> Any? {
        guard let page = page else {
            return nil;
        }
        let model = await MoyaNetworkManager.sharedInstance.asyncRequest(command: .resourceAdvertPage(["page": page]))
        return model.data
    }
}

客户端(吃烤肉串的客人)

  • Task可以打破async与await之间的相互依赖。

  • Task用闭包的形式提供了async函数的运行环境,但是与回调的数据传递方式有本质区别

  • 用回调方式的Alamofire封装,与写成async函数的Moya封装,在使用者层面差距很大。

    func getBanners() {
//        let strURL = "https://test.pandabuy.com/gateway/resource/advert/page"
//        let parameter: [String: Any] = [
//            "page": "home",
//        ]
//
//        AFNetRequest().requestData(URLString: strURL, type: .get, parameters: parameter) { responseObject, error in
//            if error != nil {
//                print("Error: \(error?.description ?? "")")
//                return
//            }
//
//            if let _ = responseObject {
//                self.banners = responseObject?["data"] as! [[String: AnyObject]]
//                self.setUI()
//            }
//        }
        
        Task {
            if let data = await ResourceApi.asyncAdvertPage("home") as? [[String: AnyObject]] {
                self.banners = data
                self.setUI()
            }
        }
    }

文件夹结构

  • 与一开始的命令模式结构图对应,相应的文件夹结构如下:
企业微信截图_32e04868-5231-4e41-be02-339311089d80.png

小结

  • Moya与AFNetworking的封装思路完全不一样

  • 经过这么一次折腾,让我回想起了曾经学过的命令模式,印象更深了一点,这波感觉不亏。大话设计模式-命令模式-2020-10-27

参考文章

Swift之Moya使用和封装
Moya

相关文章

  • Moya 上传数据的两种方式

    1、前言 Moya网络请求封装不懂可以点击Moya封装[https://www.jianshu.com/write...

  • iOS开发常用库

    pod 'Moya' 网络请求的封装 pod 'Moya/RxSwift' http://www.jianshu...

  • Moya(I)

    安装 Moya 介绍  Moya 是一个基于 Alamofire 的更高层网络请求封装抽象层。Moya 也就可以...

  • Moya + ObjectMapper + RxSwift

    使用此三剑客,访问网络更加优雅。 Moya Moya基于Alamofire的更高层网络请求封装抽象层.使用Moya...

  • Moya

    导入pod 'Moya',github地址 Moya是对Alamofire的二次封装 代码演示 协议TargetT...

  • Swift之Moya使用和封装

    Moya是什么? Moya是对请求库Alamofire的抽象封装,相当于OC中YTKNetwork和AFNetwo...

  • moya的使用

    Moya的使用 关于Moya Moya是对Alamofire的再次封装。 让我们用一张图来简单来对比一下直接用Al...

  • Swift-自己封装一个网络请求,基于Rxswift、Moya、

    近来项目需求,需要用到了Rxswift和Moya,但是网上很多例子Moya都是基于Mapper封装,而Mapper...

  • Moya基本用法

    Moya简介 一句话来讲,Moya是对Alamofire的封装,让开发人员更加优雅的使用Alamofire。 基本...

  • Swift Moya的简单使用

    Moya是对Alamofire的再次封装。 我们用一张图来对比一下Alamofire和moya的区别: 有关Ala...

网友评论

    本文标题:Moya封装实践 2023-05-13 周六

    本文链接:https://www.haomeiwen.com/subject/pugesdtx.html