Swift - 网络抽象层库Moya的使用详解8(创建自定义插件)
有时我们在发起网络请求的前后要做一些通用的操作,比如在请求时页面上会显示一个 loading 加载指示器,请求发生错误时会弹出一个告警提示框显示错误信息。
(2)RequestAlertPlugin.swift(请求状态指示插件)
(3)下面是插件使用样例。我们在创建 Provider 时将需要使用的插件传入即可。
(2)AuthPlugin.swift(授权插件)
(3)下面是插件使用样例。我们在创建 Provider 时将需要使用的插件传入即可。
(2)下面是使用样例。可以看到我们初始化插件的时候并不需要传入具体的 token 值,而是将 token 值保存在 TokenSource 对象中。后面如果发起请求会自动从该对象中获取 token 值。
(2)这里我们对请求 target 做如下修改,其中第一个请求需要授权,第二个请求不需要授权。
(3)插件的使用方法同之前的没什么不同。只不过第一个请求会自动在 header 中添加 token,而第二个请求不会。
又比如我们应用登录后会得到一个 token 令牌,后面的所有网络请求都需要在 header 中附上这个 token。
上面这些行为如果在每个请求里都写一遍,不仅麻烦,而且也会让代码变得冗余难以维护。我们可以将这些通用的行为都封装成一个个插件,使用时只需要的在对应请求中配置下即可。
十三、自定义一个请求状态指示插件
下面我们通过自定义插件实现网络活动状态提示功能。请求时会自动显示一个活动状态指示器,告知用户当前正在请求数据。如果请求错误还会自动显示错误信息。
1,样例代码
(1)DouBanAPI.swift(网络请求层)
这里面的 target 定义和之前的没什么不同。由于本节主要介绍的是插件的使用,这里面代码就简单些,只定义了一种请求类型。
import Moya //请求分类 public enum DouBan { case channels //获取频道列表 } //请求配置 extension DouBan: TargetType { //服务器地址 public var baseURL: URL { switch self { case .channels: return URL(string: "https://www.douban.com")! } } //各个请求的具体路径 public var path: String { switch self { case .channels: return "/j/app/radio/channels" } } //请求类型 public var method: Moya.Method { return .get } //请求任务事件(这里附带上参数) public var task: Task { return .requestPlain } //是否执行Alamofire验证 public var validate: Bool { return false } //这个就是做单元测试模拟的数据,只会在单元测试文件中有作用 public var sampleData: Data { return "{}".data(using: String.Encoding.utf8)! } //请求头 public var headers: [String: String]? { return nil } }
(2)RequestAlertPlugin.swift(请求状态指示插件)
这个是本文的重点,自定义插件要实现 PluginType 协议接口。该协议定义了如下四个方法分别对应请求的四个阶段:
- prepare:准备发起请求。我们可以在这里对请求进行修改,比如再增加一些额外的参数。
- willSend:开始发起请求。我们可以在这里显示网络状态指示器。
- didReceive:收到请求响应。我们可以在这里根据结果自动进行一些处理,比如请求失败时将失败信息告诉用户,或者记录到日志中。
- process:处理请求结果。我们可以在 completion 前对结果进行进一步处理。
import UIKit import Moya import Result final class RequestAlertPlugin: PluginType { //当前的视图控制器 private let viewController: UIViewController //活动状态指示器(菊花进度条) private var spinner: UIActivityIndicatorView! //插件初始化的时候传入当前的视图控制器 init(viewController: UIViewController) { self.viewController = viewController //初始化活动状态指示器 self.spinner = UIActivityIndicatorView(activityIndicatorStyle: .gray) self.spinner.center = self.viewController.view.center } //开始发起请求 func willSend(_ request: RequestType, target: TargetType) { //请求时在界面中央显示一个活动状态指示器 viewController.view.addSubview(spinner) spinner.startAnimating() } //收到请求 func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) { //移除界面中央的活动状态指示器 spinner.removeFromSuperview() spinner.stopAnimating() //只有请求错误时会继续往下执行 guard case let Result.failure(error) = result else { return } //弹出并显示错误信息 let message = error.errorDescription ?? "未知错误" let alertViewController = UIAlertController(title: "请求失败", message: "\(message)", preferredStyle: .alert) alertViewController.addAction(UIAlertAction(title: "确定", style: .default, handler: nil)) viewController.present(alertViewController, animated: true) } }
(3)下面是插件使用样例。我们在创建 Provider 时将需要使用的插件传入即可。
//初始化豆瓣FM请求的provider(并使用自定义插件) let DouBanProvider = MoyaProvider<DouBan>(plugins: [ RequestAlertPlugin(viewController: self) ]) //使用我们的provider进行网络请求(获取频道列表数据) DouBanProvider.request(.channels) { result in if case let .success(response) = result { //解析数据 let data = try? response.mapJSON() let json = JSON(data!) self.channels = json["channels"].arrayValue //刷新表格数据 DispatchQueue.main.async{ self.tableView.reloadData() } } }
2,效果图
(1)我们在发起网络请求时,界面中央会自动显示一个旋转的活动状态指示器。
(2)收到请求响应后,会自动移除界面上的状态指示器。
(3)如果请求失败,还会通过告警提示框将错误信息弹出显示。
十四、自定义一个授权插件
通过 JWT(JSON Web 令牌)或其他类型的访问令牌来授权 API 请求在开发中比较常见的。我们下面创建一个可用于自动向请求添加令牌的插件。这样就省的每个请求再去手动一个个添加令牌。
HTTP Basic认证介绍:
浏览器和 web 服务器之间可以通过 cookie 来身份识别。而桌面应用程序、或者移动应用也可以通过 HTTP 协议跟 Web 服务器交互, 但桌面或移动应用程序一般不会使用 cookie,而是把“用户名+冒号+密码”用 BASE64 编码的字符串放在 http request 中的 header Authorization 中发送给服务端,这种方式叫 HTTP 基本认证(Basic Authentication)。具体认证过程如下:
浏览器和 web 服务器之间可以通过 cookie 来身份识别。而桌面应用程序、或者移动应用也可以通过 HTTP 协议跟 Web 服务器交互, 但桌面或移动应用程序一般不会使用 cookie,而是把“用户名+冒号+密码”用 BASE64 编码的字符串放在 http request 中的 header Authorization 中发送给服务端,这种方式叫 HTTP 基本认证(Basic Authentication)。具体认证过程如下:
- 当客户端每次请求数据时,会将用户名及密码以 BASE64 加密,并将密文附加于请求头(Request Header)中。
- HTTP 服务器在每次收到请求包后,根据协议取得客户端附加的用户信息(BASE64 加密的用户名和密码),解开请求包,对用户名及密码进行验证,如果用户名及密码正确,则根据客户端请求,返回客户端所需要的数据。否则,返回错误代码或重新要求客户端提供用户名及密码。
1,样例代码
(1)HttpbinAPI.swift(网络请求层)
这里面的 target 定义和之前的没什么不同。由于本节主要介绍的是插件的使用,这里面代码就简单些,只定义了一种请求类型。
import Moya //请求分类 public enum Httpbin { case anything //请求数据 } //请求配置 extension Httpbin: TargetType { //服务器地址 public var baseURL: URL { return URL(string: "http://httpbin.org")! } //各个请求的具体路径 public var path: String { switch self { case .anything: return "/anything" } } //请求类型 public var method: Moya.Method { return .post } //请求任务事件(这里附带上参数) public var task: Task { return .requestPlain } //是否执行Alamofire验证 public var validate: Bool { return false } //这个就是做单元测试模拟的数据,只会在单元测试文件中有作用 public var sampleData: Data { return "{}".data(using: String.Encoding.utf8)! } //请求头 public var headers: [String: String]? { return nil } }
(2)AuthPlugin.swift(授权插件)
这个是本文的重点,自定义插件要实现 PluginType 协议接口。该协议定义的四个方法上面介绍过了,这里我们只要实现 prepare 方法,在准备发起请求时对请求进行修改:增加一个名为“Authorization”的 http header,其内容为传入的 token 值。
import Foundation import Moya struct AuthPlugin: PluginType { //令牌字符串 let token: String //准备发起请求 func prepare(_ request: URLRequest, target: TargetType) -> URLRequest { var request = request //将token添加到请求头中 request.addValue(token, forHTTPHeaderField: "Authorization") return request } }
//初始化请求的provider(并使用自定义插件) let HttpbinProvider = MoyaProvider<Httpbin>(plugins: [ AuthPlugin(token: "hangge12345") ]) //使用我们的provider进行网络请求(获取频道列表数据) HttpbinProvider.request(.anything) { result in if case let .success(response) = result { //解析数据 let data = try? response.mapJSON() let json = JSON(data!) //... } }
2,效果图
可以看到当我们发起请求时,token 会自动添加到请求头(http header)中。
3,功能改进一:发起请求时再去获取 token
(1)有时我们在创建插件的时候并不知道 tocken 值(可能需要登录后才能得到)。这里对插件做个修改,改成请求时从一个 token 数据源对象中获取 token 值。
import Foundation import Moya //用于存储令牌字符串 class TokenSource { var token: String? init() { } } struct AuthPlugin: PluginType { //获取令牌字符串方法 let tokenClosure: () -> String? //准备发起请求 func prepare(_ request: URLRequest, target: TargetType) -> URLRequest { var request = request //获取获取令牌字符串 if let token = tokenClosure() { //将token添加到请求头中 request.addValue(token, forHTTPHeaderField: "Authorization") } return request } }
(2)下面是使用样例。可以看到我们初始化插件的时候并不需要传入具体的 token 值,而是将 token 值保存在 TokenSource 对象中。后面如果发起请求会自动从该对象中获取 token 值。
//定义token源对象 let source = TokenSource() //初始化豆瓣FM请求的provider(并使用自定义插件) let HttpbinProvider = MoyaProvider<Httpbin>(plugins: [ AuthPlugin(tokenClosure: { return source.token }) ]) //设置token串 source.token = "hangge12345" //使用我们的provider进行网络请求(获取频道列表数据) HttpbinProvider.request(.anything) { result in if case let .success(response) = result { //解析数据 let data = try? response.mapJSON() let json = JSON(data!) //... } }
4,功能改进二:指定需要授权的请求
(1)有时可能并不是所有的请求都需要授权。我们可以通过扩展 TargetType 协议,增加一个是否需要授权的属性,然后根据返回值决定这个请求是否需要在 header 中添加授权令牌。
import Foundation import Moya protocol AuthorizedTargetType: TargetType { //返回是否需要授权 var needsAuth: Bool { get } } struct AuthPlugin: PluginType { //令牌字符串 let token: String //准备发起请求 func prepare(_ request: URLRequest, target: TargetType) -> URLRequest { //判断该请求是否需要授权 guard let target = target as? AuthorizedTargetType, target.needsAuth else { return request } var request = request //将token添加到请求头中 request.addValue(token, forHTTPHeaderField: "Authorization") return request } }
(2)这里我们对请求 target 做如下修改,其中第一个请求需要授权,第二个请求不需要授权。
import Moya //请求分类 public enum Httpbin { case anything //请求1(该请求需要授权) case uuid //请求2(该请求不需要授权) } //请求配置 extension Httpbin: AuthorizedTargetType { //服务器地址 public var baseURL: URL { return URL(string: "http://httpbin.org")! } //各个请求的具体路径 public var path: String { switch self { case .anything: return "/anything" case .uuid: return "/uuid" } } //请求类型 public var method: Moya.Method { return .get } //请求任务事件(这里附带上参数) public var task: Task { return .requestPlain } //是否执行Alamofire验证 public var validate: Bool { return false } //这个就是做单元测试模拟的数据,只会在单元测试文件中有作用 public var sampleData: Data { return "{}".data(using: String.Encoding.utf8)! } //请求头 public var headers: [String: String]? { return nil } //是否需要授权 public var needsAuth: Bool { switch self { case .anything: return true case .uuid: return false } } }
(3)插件的使用方法同之前的没什么不同。只不过第一个请求会自动在 header 中添加 token,而第二个请求不会。
//初始化请求的provider(并使用自定义插件) let HttpbinProvider = MoyaProvider<Httpbin>(plugins: [ AuthPlugin(token: "hangge12345") ]) //使用我们的provider进行网络请求(该请求需要授权) HttpbinProvider.request(.anything) { result in if case let .success(response) = result { //解析数据 let data = try? response.mapJSON() let json = JSON(data!) //... } } //使用我们的provider进行网络请求(该请求不需要授权) HttpbinProvider.request(.uuid) { result in if case let .success(response) = result { //解析数据 let data = try? response.mapJSON() let json = JSON(data!) //... } }
航哥 我是新人 坚持 加油哦 自勉