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

简介

从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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,392评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,258评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,417评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,992评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,930评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,199评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,652评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,327评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,463评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,382评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,432评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,118评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,704评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,787评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,999评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,476评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,057评论 2 341

推荐阅读更多精彩内容