简介
从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)
}
}
}
- 下面这篇文章讲解了详细的原理,值得好好学习:
闲话 Swift 协程(2):将回调改写成 async 函数
封装形式
-
在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()
}
}
}
文件夹结构
- 与一开始的命令模式结构图对应,相应的文件夹结构如下:
小结
-
Moya与AFNetworking的封装思路完全不一样
-
经过这么一次折腾,让我回想起了曾经学过的命令模式,印象更深了一点,这波感觉不亏。大话设计模式-命令模式-2020-10-27
网友评论