Swift:面向协议的网络请求

栏目: Swift · 发布时间: 5年前

内容简介:在上述面向对象的实现中我们可以看到发起一个网络请求一般会有三个步骤我们可以把这三个步骤进行抽象,用三个
class Light {
  func 插电() {}
  func 打开() {}
  func 增大亮度() {}
  func 减小亮度() {}
}

class LEDLight: Light {}
class DeskLamp: Light {}

func 打开(物体: Light) {
  物体.插电()
  物体.打开()
}

func main() {
  打开(物体: DeskLamp())
  打开(物体: LEDLight())
}
复制代码

在上述面向对象的实现中 打开 方法似乎只局限于 Light 这个类和他的派生类。如果我们想描述 打开 这个操作并且不单单局限于 Light 这个类和他的派生类,(毕竟柜子、桌子等其他物体也是可以打开的)抽象 打开 这个操作,那么 protocol 就可以派上用场了。

protocol Openable {
  func 准备工作()
  func 打开()
}

extension Openable {
  func 准备工作() {}
  func 打开() {}
}

class LEDLight: Openable {}
class DeskLamp: Openable {}
class Desk: Openable {}

func 打开<T: Openable>(物体: T) {
  物体.准备工作()
  物体.打开()
}

func main() {
  打开(物体: Desk())
  打开(物体: LEDLight())
}
复制代码

普通的网络请求

// 1.准备请求体
  let urlString = "https://www.baidu.com/user"
  guard let url = URL(string: urlString) else {
    return
  }
  let body = prepareBody()
  let headers = ["token": "thisisatesttokenvalue"]
  var request = URLRequest(url: url)
  request.httpBody = body
  request.allHTTPHeaderFields = headers
  request.httpMethod = "GET"

  // 2.使用URLSeesion创建网络任务
  URLSession.shared.dataTask(with: request) { (data, response, error) in
    if let data = data {
      // 3.将数据反序列化
    }
  }.resume()
复制代码

我们可以看到发起一个网络请求一般会有三个步骤

  • 准备请求体(URL、parameters、body、headers...)
  • 使用框架创建网络任务(URLSession、Alamofire、AFN...)
  • 将数据反序列化(Codable、Protobuf、SwiftyJSON、YYModel...)

我们可以把这三个步骤进行抽象,用三个 protocol 进行规范. 规范好之后,再由各个类型实现这三个协议,就可以随意组合使用.

抽象网络请求步骤

Parsable

首先我们定义 Parsable 协议来抽象反序列化这个过程

protocol Parsable {
  static func parse(data: Data) -> Result<Self>
}
复制代码

Parsable 协议定义了一个静态方法,这个方法可以从Data -> Self 例如 User 遵循 Parsable 协议,就要实现从Data转换到User的 parse(:) 方法

struct User {
  var name: String
}
extension User: Parsable {
  static func parse(data: Data) -> Result<User> {
    // ...实现Data转User
  }
}
复制代码

Codable

我们可以利用 swift协议扩展 的特性给遵循 Codable 的类型添加一个默认的实现

extension Parsable where Self: Decodable {
  static func parse(data: Data) -> Result<Self> {
    do {
      let model = try decoder.decode(self, from: data)
      return .success(model)
    } catch let error {
      return .failure(error)
    }
  }
}
复制代码

这样 User 如果遵循了 Codable ,就无需实现 parse(:) 方法了 于是反序列化的过程就变这样简单的一句话

extension User: Codable, Parsable {}

URLSession.shared.dataTask(with: request) { (data, response, error) in
    if let data = data {
      // 3.将数据反序列化
        let user = User.parse(data: data)
    }
复制代码

到这里可以想一个问题,如果data是个模型数组该怎么办?是不是在 Parsable 协议里再添加一个方法返回一个模型数组?然后再实现一遍?

public protocol Parsable {
  static func parse(data: Data) -> Result<Self>
// 返回一个数组
  static func parse(data: Data) -> Result<[Self]>
}
复制代码

这样也不是不行,但是还有更swift的方法,这种方法swift称之为条件遵循

// 当Array里的元素遵循Parsable以及Decodable时,Array也遵循Parsable协议
extension Array: Parsable where Array.Element: (Parsable & Decodable) {}
复制代码
URLSession.shared.dataTask(with: request) { (data, response, error) in
    if let data = data {
      // 3.将数据反序列化
        let users = [User].parse(data: data)
    }
复制代码

从这里可以看到swift协议是非常强大的,使用好了可以少些很多代码,在swift标准库中有很多这样的例子。

protobuf

当然,如果你使用 SwiftProtobuf ,也可以提供它的默认实现

extension Parsable where Self: SwiftProtobuf.Message {
  static func parse(data: Data) -> Result<Self> {
    do {
      let model = try self.init(serializedData: data)
      return .success(model)
    } catch let error {
      return .failure(error)
    }
  }
}
复制代码

反序列化的过程也和刚才的例子一样,调用 parse(:) 方法即可

Request

现在我们定义 Request 协议来抽象准备请求体这个过程

protocol Request {
  var url: String { get }
  var method: HTTPMethod { get }
  var parameters: [String: Any]? { get }
  var headers: HTTPHeaders? { get }
  var httpBody: Data? { get }

  /// 请求返回类型(需遵循Parsable协议)
  associatedtype Response: Parsable
}
复制代码

我们定义了一个关联类型:遵循 ParsableResponse 是为了让实现这个协议的类型指定这个请求返回的类型,限定 Response 必须遵循 Parsable 是因为,我们会用到 parse(:) 方法来进行反序列化。

我们来实现一个通用的请求体

struct NormalRequest<T: Parsable>: Request {
  var url: String
  var method: HTTPMethod
  var parameters: [String: Any]?
  var headers: HTTPHeaders?
  var httpBody: Data?

  typealias Response = T

  init(_ responseType: Response.Type,
       urlString: String,
       method: HTTPMethod = .get,
       parameters: [String: Any]? = nil,
       headers: HTTPHeaders? = nil,
       httpBody: Data? = nil) {
    self.url = urlString
    self.method = method
    self.parameters = parameters
    self.headers = headers
    self.httpBody = httpBody
  }
}
复制代码

是这样使用的

let request = NormalRequest(User.self, urlString: "https://www.baidu.com/user")
复制代码

如果服务端有一组接口 https://www.baidu.com/user https://www.baidu.com/manager https://www.baidu.com/driver 我们可以定义一个 BaiduRequest ,把URL或者公共的headers和body拿到 BaiduRequest 管理

// BaiduRequest.swift
private let host = "https://www.baidu.com"

enum BaiduPath: String {
  case user = "/user"
  case manager = "/manager"
  case driver = "/driver"
}

struct BaiduRequest<T: Parsable>: Request {
  var url: String
  var method: HTTPMethod
  var parameters: [String: Any]?
  var headers: HTTPHeaders?
  var httpBody: Data?

  typealias Response = T

  init(_ responseType: Response.Type,
       path: BaiduPath,
       method: HTTPMethod = .get,
       parameters: [String: Any]? = nil,
       headers: HTTPHeaders? = nil,
       httpBody: Data? = nil) {
    self.url = host + path.rawValue
    self.method = method
    self.parameters = parameters
    self.httpBody = httpBody
    self.headers = headers
  }
}
复制代码

创建也很简单

let userRequest = BaiduRequest(User.self, path: .user)
let managerRequest = BaiduRequest(Manager.self, path: .manager, method: .post)
复制代码

Client

最后我们定义 Client 协议,抽象发起网络请求的过程

enum Result<T> {
  case success(T)
  case failure(Error)
}
typealias Handler<T> = (Result<T>) -> ()

protocol Client {
// 接受一个遵循Parsable的T,最后回调闭包的参数是T里边的Response 也就是Request协议定义的Response
  func send<T: Request>(request: T, completionHandler: @escaping Handler<T.Response>)
}
复制代码

URLSession

我们来实现一个使用 URLSessionClient

struct URLSessionClient: Client {
  static let shared = URLSessionClient()
  private init() {}

  func send<T: Request>(request: T, completionHandler: @escaping (Result<T.Response>) -> ()) {
    var urlString = request.url
    if let param = request.parameters {
      var i = 0
      param.forEach {
        urlString += i == 0 ? "?\($0.key)=\($0.value)" : "&\($0.key)=\($0.value)"
        i += 1
      }
    }
    guard let url = URL(string: urlString) else {
      return
    }
    var req = URLRequest(url: url)
    req.httpMethod = request.method.rawValue
    req.httpBody = request.httpBody
    req.allHTTPHeaderFields = request.headers

    URLSession.shared.dataTask(with: req) { (data, respose, error) in
      if let data = data {
        // 使用parse方法反序列化
        let result = T.Response.parse(data: data)
        switch result {
        case .success(let model):
          completionHandler(.success(model))
        case .failure(let error):
          completionHandler(.failure(error))
        }
      } else {
        completionHandler(.failure(error!))
      }
    }
  }
}
复制代码

三个协议实现好之后 例子开头的网络请求就可以这样写了

let request = NormalRequest(User.self, urlString: "https://www.baidu.com/user")
URLSessionClient.shared.send(request) { (result) in
  switch result {
     case .success(let user):
       // 此时拿到的已经是User实例了
       print("user: \(user)")
     case .failure(let error):
       printLog("get user failure: \(error)")
     }
}
复制代码

Alamofire

当然也可以用 Alamofire 实现 Client

struct NetworkClient: Client {
  static let shared = NetworkClient()

  func send<T: Request>(request: T, completionHandler: @escaping Handler<T.Response>) {
    let method = Alamofire.HTTPMethod(rawValue: request.method.rawValue) ?? .get
    var dataRequest: Alamofire.DataRequest

    if let body = request.httpBody {
      var urlString = request.url
      if let param = request.parameters {
        var i = 0
        param.forEach {
          urlString += i == 0 ? "?\($0.key)=\($0.value)" : "&\($0.key)=\($0.value)"
          i += 1
        }
      }
      guard let url = URL(string: urlString) else {
        print("URL格式错误")
        return
      }
      var urlRequest = URLRequest(url: url)
      urlRequest.httpMethod = method.rawValue
      urlRequest.httpBody = body
      urlRequest.allHTTPHeaderFields = request.headers
      dataRequest = Alamofire.request(urlRequest)
    } else {
      dataRequest = Alamofire.request(request.url,
                                      method: method,
                                      parameters: request.parameters,
                                      headers: request.headers)
    }

    dataRequest.responseData { (response) in
      switch response.result {
      case .success(let data):
        // 使用parse(:)方法反序列化
        let parseResult = T.Response.parse(data: data)
        switch parseResult {
        case .success(let model):
          completionHandler(.success(model))
        case .failure(let error):
          completionHandler(.failure(error))
        }
      case .failure(let error):
        completionHandler(.failure(error))
      }
    }
  }

  private init() {}
}
复制代码

我们试着发起一组网络请求

let userRequest = BaiduRequest(User.self, path: .user)
let managerRequest = BaiduRequest(Manager.self, path: .manager, method: .post)

NetworkClient.shared.send(managerRequest) { result in
    switch result {
     case .success(let manager):
       // 此时拿到的已经是Manager实例了
       print("manager: \(manager)")
     case .failure(let error):
       printLog("get manager failure: \(error)")
     }
}
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

爆款:如何打造超级IP

爆款:如何打造超级IP

【美】安妮塔•埃尔伯斯 / 杨雨 / 中信出版社 / 2016-1-10 / 49

哈佛商学院IP运营与产品管理方法论第一书,翻转长尾理论的重要著作! 电影大片、当红炸子鸡、百万畅销书背后的运营逻辑是什么? 《五十度灰》、Lady Gaga、维多利亚的秘密有何共同秘密? 漫威如何将蜘蛛侠、X战警、绿巨人打造成金矿? 皇马如何打造体育IP,一跃成为全球收 入最高的足球俱乐部? 爆款策略如何运用于电影、电视、音乐、出版、体育与商业各领域? ----......一起来看看 《爆款:如何打造超级IP》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换