------ 2019.11.24 update 新增了另外一种封装思路,写在最后,下面的是正文。------
踩坑踩了4天总算把基于Moya的网络框架搭建完毕
看网上关于Moya的教程不太多,大多都是一样的,还有一些年久失修。这里专门讲讲关于moya的搭建及容易遇到的一些坑。
重要的东西放到最前面
1.最好的教材是官方文档和Demo,Moya有中文文档。
2.尝试一些不一样的东西会让开发更有趣。
3.我把Demo地址放最后了。
为什么选择moya:
一开始网络框架的选型有Alamofire和Moya。
Alamofire可以说是Swift版本的AFN,啃AFN的老啃了几年了,AFN的确博大精深,有很多值得开发者去学习的地方。但开发这么多年,AFN实在是啃不动了。试着封装了一下Alamofire。感觉和AFN封装大同小异。
和技术群里的一些大佬讨论了一下,大多数也是推荐Moya,至于聊天记录里面提及的包含?地址的问题我们在稍后的内容里去解决。后来咬咬牙就决定使用Moya用新项目的网络框架。
About Moya
已经有大神把Moya的基本使用和各个模块的介绍说的很清楚了,这里就不赘述了,建议把框架的基本使用了解一番【iOS开发】Moya入坑记-用法解读篇
上文作为入门是一篇不错的文章,但作为实际开发过程中,健壮全方位考虑的网络框架来说的来说还有很多用法并没有提及。 而且网上很多文章都是老版本,看的时候会感觉有些懵。。。所以我就写了本文
Let's Begin
封装的目录结构
安装好Moya后我们创建好三个空的Swift文件
我们大致可将网络框架拆分成
API.swift---将来我们的接口列表和不同的接口的一些配置在里面完成,最长打交道的地方。
NetworkManager.swift---基本框架配置及封装写到这里
MoyaConfig.swift---这个其实可有可无的,习惯上把baseURL和一些公用字符串放进来
OK我们正式开始coding!
API.swift中先创建一个API的枚举,枚举值是接口名, 并创建遵守TargetType协议的extention。
这里我写三个测试的Api。第一个是无参,第二个是普通写法(我看官方文档好像是这种多参数都写进去的,实际开发过程中感觉有些麻烦),第三个是直接把所有参数包装成字典传进来的文艺写法。。
直接点击错误代码补全即可自动补全所有的协议
import Foundation
import Moya
enum API {
case testApi//无参数的接口
//有参数的接口
case testAPi(para1:String,para2:String)//普遍的写法
case testApiDict(Dict:[String:Any])//把参数包装成字典传入--推荐使用
}
extension API:TargetType{
//baseURL 也可以用枚举来区分不同的baseURL,不过一般也只有一个BaseURL
var baseURL: URL {
return URL.init(string: "http://news-at.zhihu.com/api/")!
}
//不同接口的字路径
var path: String {
switch self {
case .testApi:
return "4/news/latest"
case .testAPi(let para1, _):
return "\(para1)/news/latest"
case .testApiDict:
return "4/news/latest"
// default:
// return "4/news/latest"
}
}
/// 请求方式 get post put delete
var method: Moya.Method {
switch self {
case .testApi:
return .get
default:
return .post
}
}
/// 这个是做单元测试模拟的数据,必须要实现,只在单元测试文件中有作用
var sampleData: Data {
return "".data(using: String.Encoding.utf8)!
}
/// 这个就是API里面的核心。嗯。。至少我认为是核心,因为我就被这个坑过
//类似理解为AFN里的URLRequest
var task: Task {
switch self {
case .testApi:
return .requestPlain
case let .testAPi(para1, _)://这里的缺点就是多个参数会导致parameters拼接过长
//后台的content-Type 为application/x-www-form-urlencoded时选择URLEncoding
return .requestParameters(parameters: ["key":para1], encoding: URLEncoding.default)
case let .testApiDict(dict)://所有参数当一个字典进来完事。
//后台可以接收json字符串做参数时选这个
return .requestParameters(parameters: dict, encoding: JSONEncoding.default)
}
}
/// 设置请求头header
var headers: [String : String]? {
//同task,具体选择看后台 有application/x-www-form-urlencoded 、application/json
return ["Content-Type":"application/x-www-form-urlencoded"]
}
}
上面api.swift设置完毕
NetworkManager.swift
下面就开始构建我们的请求相关的东西
主要是完成对于Provider的完善及个性化设置。
首先先看一个最简单的网络请求, 我们所有的请求都是来自于这个provider对象,测试一下 我们就能发出请求并拿到返回的结果。
let provier = MoyaProvider()
provier.request(.testApi) { (result) in
switch result {
case let .success(response):
print(response)
case let .failure(error):
print("网络连接失败")
break
}
}
当然,对应情况复杂的项目这个是远远不够滴!
so~ 下面开始对provider进行改造
先看看最丰满的provider是什么样子的
当我看到这一个个扑朔迷离的参数时我的表情是这样的(⊙﹏⊙)b
点进去看源码才发现Moya已经帮我们把每个参数都默认实现了一遍。我们可以根据自己的设计需求设置参数
每个参数什么意思也不赘述了,Moya 的初始化 这篇文章也都说了,建议初学者阅读一下。
需要指正的地方是:
文中 endpointClosure 的使用举例中 target.parameters 已经没有这个属性了。现在版本的Moya用的task代替的。
Moya官方不希望在所有的请求中统一添加参数,不过我们可以自己去定义endPointClosure实现相应的效果
详情参照:Add additional parameters to all requests 里面有具体的解决方案。 Demo里面已经把这个需求写入了。
去除了不太常用的自定义stubClosure, callbackQueue, trackInflights后我的Provider长这样
import Foundation
import Moya
import Alamofire
import SwiftyJSON
/// 超时时长
private var requestTimeOut:Double = 30
///endpointClosure
private let myEndpointClosure = { (target: API) -> Endpoint in
///这里的endpointClosure和网上其他实现有些不太一样。
///主要是为了解决URL带有?无法请求正确的链接地址的bug
let url = target.baseURL.absoluteString + target.path
var endpoint = Endpoint(
url: url,
sampleResponseClosure: { .networkResponse(200, target.sampleData) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers
)
switch target {
case .easyRequset:
return endpoint
case .register:
requestTimeOut = 5//按照项目需求针对单个API设置不同的超时时长
return endpoint
default:
requestTimeOut = 30//设置默认的超时时长
return endpoint
}
}
private let requestClosure = { (endpoint: Endpoint, done: MoyaProvider.RequestResultClosure) in
do {
var request = try endpoint.urlRequest()
//设置请求时长
request.timeoutInterval = requestTimeOut
// 打印请求参数
if let requestData = request.httpBody {
print("\(request.url!)"+"\n"+"\(request.httpMethod ?? "")"+"发送参数"+"\(String(data: request.httpBody!, encoding: String.Encoding.utf8) ?? "")")
}else{
print("\(request.url!)"+"\(String(describing: request.httpMethod))")
}
done(.success(request))
} catch {
done(.failure(MoyaError.underlying(error, nil)))
}
}
/* 设置ssl
let policies: [String: ServerTrustPolicy] = [
"example.com": .pinPublicKeys(
publicKeys: ServerTrustPolicy.publicKeysInBundle(),
validateCertificateChain: true,
validateHost: true
)
]
*/
// 用Moya默认的Manager还是Alamofire的Manager看实际需求。HTTPS就要手动实现Manager了
//private public func defaultAlamofireManager() -> Manager {
//
// let configuration = URLSessionConfiguration.default
//
// configuration.httpAdditionalHeaders = Alamofire.SessionManager.defaultHTTPHeaders
//
// let policies: [String: ServerTrustPolicy] = [
// "ap.grtstar.cn": .disableEvaluation
// ]
// let manager = Alamofire.SessionManager(configuration: configuration,serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies))
//
// manager.startRequestsImmediately = false
//
// return manager
//}
/// NetworkActivityPlugin插件用来监听网络请求
private let networkPlugin = NetworkActivityPlugin.init { (changeType, targetType) in
print("networkPlugin \(changeType)")
//targetType 是当前请求的基本信息
switch(changeType){
case .began:
print("开始请求网络")
case .ended:
print("结束")
}
}
// https://github.com/Moya/Moya/blob/master/docs/Providers.md 参数使用说明
//stubClosure 用来延时发送网络请求
let Provider = MoyaProvider(endpointClosure: myEndpointClosure, requestClosure: requestClosure, plugins: [networkPlugin], trackInflights: false)
NetworkManager.swift 基本写完 还剩一点下面再说。
这个时候我们的网络请求就会长这样:
Provider.request(.testApi) { (result) in
switch result {
case let .success(response):
print(response)
//做相应的数据处理 这里我用的是HandyJson
case let .failure(error):
print("网络连接失败")
//提示用户网络链接失败
break
}
}
像我这种懒得一比的开发者,当然不想每一次都写这么多result判断。写好多重复的代码。
于是我决定再次封装。。。
来来,我们再次回到NetworkManager.swift 封装provider请求。
思路:
1.后台返回错误的时候我统一把errormsg显示给用户
2.只有返回正确的时候才把数据提取出来进行解析。 对应的网络请求的hud全部封装到请求里面。
这个是针对于大多数请求。个别展示效果不同的请求自己老老实实用provider.request写就行。
下面我们在NetworkManager.swift中进行二次封装
///先添加一个闭包用于成功时后台返回数据的回调
typealias successCallback = ((String) -> (Void))
///再次用一个方法封装provider.request()
func NetWorkRequest(_ target: API, completion: @escaping successCallback ){
//先判断网络是否有链接 没有的话直接返回--代码略
//显示hud
Provider.request(target) { (result) in
//隐藏hud
switch result {
case let .success(response):
do {
//这里转JSON用的swiftyJSON框架
let jsonData = try JSON(data: response.data)
//判断后台返回的code码没问题就把数据闭包返回 ,我们后台是0000 以实际后台约定为准。
if jsonData[RESULT_CODE].stringValue == "0000"{
completion(String(data: response.data, encoding: String.Encoding.utf8)!)
}else{
//flag 不为0000 HUD显示错误信息
print("flag不为0000 HUD显示后台返回message"+"\(jsonData[RESULT_MESSAGE].stringValue)")
}
} catch {
}
case let .failure(error):
guard let error = error as? CustomStringConvertible else {
//网络连接失败,提示用户
print("网络连接失败")
break
}
}
}
}
MoyaConfig.swift 这个就是丢一些公用字符串
觉得麻烦可以放在NetworkManager.swift中 看个人爱好
代码如下
import Foundation
/// 定义基础域名
let Moya_baseURL = "http://news-at.zhihu.com/api/"
/// 定义返回的JSON数据字段
let RESULT_CODE = "flag" //状态码
let RESULT_MESSAGE = "message" //错误消息提示
这个时候我们再去用封装好的网络工具优雅的进行网络请求
NetWorkRequest(.testApi) { (response) -> (Void) in
//用HandyJSON对返回的数据进行处理
}
ps.
1.关于在所有的请求中统一添加token方法
https://github.com/Moya/Moya/issues/1482 这个issue中moya的开发者已经有对应的解决方案:
就是在endpoint中统一修改target.task 不过也并没有这么麻烦,只要改你对应请求中的task就行。
这个我github上最新版已经同步。
2.下面有评论问道项目中会有很多的case。的确是这样的,但实际使用起来的愉悦感还是很强的,顺便po一张已上线项目里的API枚举的case:
enum API{
case productListPage(parameters: [String:Any])
//发送短消息
case sendSMS(parameters: [String:Any])
//产品详情
case productDetail(parameters: [String:Any])
//产品购买
case buyingProduct(parameters: [String:Any])
//服务列表
case serviceList(parameters: [String:Any])
//服务详情
case serviceDetail(parameters: [String:Any])
//购买服务
case buyingService(parameters: [String:Any])
//门店列表
case storeList(parameters: [String:Any])
//门店详情
case storeDetail(parameters: [String:Any])
//上传用户头像
case uploadHeadImage(parameters: [String:Any],imageDate:Data)
//交易历史
case dealHistory(parameters: [String:Any])
//优惠券列表
case couponList(parameters: [String:Any])
//修改密码
case modifyPwd(parameters: [String:Any])
//已购买的服务
case hadboughtService(parameters: [String:Any])
//更新用户信息
case updateUserInfo(parameters: [String:Any])
// 服务评价
case commentService(parameters: [String:Any])
//皮肤档案
case skinFile(parameters: [String:Any])
//美容轨迹--已购买的服务
case usedHistory(parameters: [String:Any])
//获取用户信息
case getUserInfo(parameters: [String:Any])
//密码登陆
case loginByPwd(parameters: [String:Any])
//短消息登陆
case loginByMsg(parameters: [String:Any])
// 获取短消息
case getMsg(parameters: [String:Any])
// 获取图形验证码
case getImgVerify(parameters: [String:Any])
// 登出
case logOut(parameters: [String:Any])
//获取服务项目列表
case serviceListOfAppoint(parameters: [String:Any])
// 美容师列表
case technicianList(parameters: [String:Any])
// 服务预约
case appointingService(parameters: [String:Any])
// 获取字典名称
case getDictDesc(parameters: [String:Any])
// 获取字典列表
case getDictList(parameters: [String:Any])
// 微信登陆
case wechatLogin(parameters: [String:Any])
// 获取票据
case getTicket(parameters: [String:Any])
// 检测结果
case checkResult(parameters: [String:Any])
//查询服务次数
case checkUserServiceCount(parameters: [String:Any])
//获取已购产品 获取会员信息
case getUserProduct(parameters: [String:Any])
//会员购买产品状态
case productBuyStatus(parameters: [String:Any])
//获取省市区
case getProvinceInfo(parameters: [String:Any])
//获取门店特色
case getStoreFeatureList(parameters: [String:Any])
//预约管理
case getOrderInfo(parameters: [String:Any])
//设置专属技师
case setExclusiveTechnician(parameters: [String:Any])
//产品特色
case getProductFeatureList(parameters: [String:Any])
//获取首页banner
case getHomeBannerList(parameters: [String:Any])
//获取预约时间
case getOrderTimeList(parameters: [String:Any])
///我的特权
case getMyprivilege(parameters: [String:Any])
///取消购买产品
case cancelBuyingProduct(parameters: [String:Any])
///取消购买服务
case cancelBuyingService(parameters: [String:Any])
///取消预约
case cancelingAppointing(parameters: [String:Any])
//
case updateAPi(parameters: [String:Any])
}
------------- 2019.11.24 update ↓ -----------
两年前我写了这篇关于Moya网络框架的封装的文章,
上面的封装思路的原则是能少写代码就少写代码。懒人专用。
随着业务的发展 API 文件中的switch case 文件越来越多。 其实个人感觉维护起来其实也还好。
最近打算再次优化,把不同模块的API封装到不同的 枚举enum 中,
这个时候遇到了一个问题 就是上面的Provider只能用于API这个枚举体的数据
。
如果要新写新的枚举体,要封装一套新的Provider了。 后来查看了一些国外开发者对Moya的封装。 有一部分是把不同模块的API封装到不同的枚举中去维护。 然后针对于不同的模块去创建Provider类,并内部对Provider 做具体的实现。
使用的时候 使用具体的Provider类的实例去做网络请求。
这样的好处是可以分开管理不同的模块(其实Moya的初衷就是取抽离网络请求和具体的业务逻辑, 已经有一点解耦的意思了)。 坏处就是代码量会稍微多一些。
具体的代码实现我也写了Demo放在的项目里面。
真正喜欢用哪个就看个人需求了~
github地址:https://github.com/Liaoworking/MoyaNetworkTool
个人技术博客地址:http://liaoworking.com