当前位置: > > > Swift - 网络抽象层库Moya的使用详解8(创建自定义插件)

Swift - 网络抽象层库Moya的使用详解8(创建自定义插件)

    有时我们在发起网络请求的前后要做一些通用的操作,比如在请求时页面上会显示一个 loading 加载指示器,请求发生错误时会弹出一个告警提示框显示错误信息。
    又比如我们应用登录后会得到一个 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)如果请求失败,还会通过告警提示框将错误信息弹出显示。
            

十四、自定义一个授权插件

通过 JWTJSON Web 令牌)或其他类型的访问令牌来授权 API 请求在开发中比较常见的。我们下面创建一个可用于自动向请求添加令牌的插件。这样就省的每个请求再去手动一个个添加令牌。
HTTP Basic认证介绍:
浏览器和 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
    }
}

(3)下面是插件使用样例。我们在创建 Provider 时将需要使用的插件传入即可。
//初始化请求的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!)
        //...
    }
}

评论1
  • 1楼
    2017-10-20 11:44
    FireHsia

    航哥 我是新人 坚持 加油哦 自勉

    站长回复

    加油,我们一起努力!