Malibu 8.1.0

Malibu 8.1.0

测试已测试
Lang语言 SwiftSwift
许可 NOASSERTION
发布上次发布2019年4月
SPM支持 SPM

Vadym MarkovHyper Interaktiv ASKhoa Pham维护。



Malibu 8.1.0

Malibu logo

CI Status Version Carthage Compatible License Platform Swift

描述

棕榈树、珊瑚礁和冲浪的海浪。欢迎来到冲浪俱乐部 Malibu,这是一个基于 promises 构建的网络库。它不仅仅是围绕 URLSession 的包装,而是一个强大的框架,帮助您链接请求、验证和处理请求。

在底层使用 WhenMalibu 增加了很多辅助函数,并将您的代码提升到下一个层次

  • 不再有“回调地狱”。
  • 您的请求在同一个地方描述。
  • 响应处理可以轻松分解为多个逻辑任务。
  • 数据和错误分别处理。
  • 您的网络代码更加干净、易于阅读,并遵循 DRY 原则。

装备好 Malibu 的必需工具,成为一个大浪冲浪者,让充满鲨鱼的异步网络变得过去式。享受这段旅程!

功能

  • 多种网络堆栈
  • 声明性请求
  • 基于 promises 的可链接响应回调
  • 所有所需的内容类型和参数编码
  • HTTP 响应验证
  • 响应数据序列化
  • 响应模拟
  • 请求、响应和错误记录
  • 同步和异步模式
  • 请求预处理和中间件
  • 请求离线存储
  • 广泛的单元测试覆盖率

目录

抓住浪潮

你马上就可以开始,无需考虑配置

// Create your request => GET http://sharkywaters.com/api/boards?type=1
let request = Request.get("http://sharkywaters.com/api/boards", parameters: ["type": 1])

// Make a call
Malibu.request(request)
  .validate()
  .toJsonDictionary()
  .then({ dictionary -> [Board] in
    // Let's say we use https://github.com/zenangst/Tailor for mapping
    return try dictionary.relationsOrThrow("boards") as [Board]
  })
  .done({ boards in
    // Handle response data
  })
  .fail({ error in
    // Handle errors
  })
  .always({ _ in
    // Hide progress bar
  })

如果你还没有看到任何好处,请继续向下滚动,准备迎接更多的魔法😉...

RequestConvertible

大部分时间我们需要分离的网络堆栈来与多个API服务一起工作。用 马兰布鲁 来实现超级简单。创建一个符合 RequestConvertible 协议的 enum,并使用所有属性描述您的请求

enum SharkywatersEndpoint: RequestConvertible {
  // Describe requests
  case fetchBoards
  case showBoard(id: Int)
  case createBoard(type: Int, title: String)
  case updateBoard(id: Int, type: Int, title: String)
  case deleteBoard(id: Int)

  // Every request will be scoped by the base url
  // Base url is recommended, but optional
  static var baseUrl: URLStringConvertible? = "http://sharkywaters.com/api/"

  // Additional headers for every request
  static var headers: [String: String] = [
    "Accept" : "application/json"
  ]

  // Build requests
  var request: Request {
    switch self {
    case .fetchBoards:
      return Request.get("boards")
    case .showBoard(let id):
      return Request.get("boards/\(id)")
    case .createBoard(let type, let title):
      return Request.post("boards", parameters: ["type": type, "title": title])
    case .updateBoard(let id, let title):
      return Request.patch("boards/\(id)", parameters: ["title": title])
    case .deleteBoard(let id):
      return Request.delete("boards/\(id)")
    }
  }
}

注意Accept-LanguageAccept-EncodingUser-Agent 头部信息将自动包含。

Request

马兰布鲁 中,使用结构体描述 Request

let request = Request(
  // HTTP method
  method: .get,
  // Request url or path
  resource: "boards",
  // Content type
  contentType: .query,
  // Request parameters
  parameters: ["type": 1, "text": "classic"],
  // Headers
  headers: ["custom": "header"],
  // Offline storage configuration
  storePolicy: .unspecified,
  // Cache policy
  cachePolicy: .useProtocolCachePolicy)

同时还有多个具有默认值的辅助方法,覆盖每个 HTTP 方法

// GET request
let getRequest = Request.get("boards")

// POST request
let postRequest = Request.post(
  "boards",
  // Content type is set to `.json` by default for POST
  contentType: .formURLEncoded,
  parameters: ["type" : kind, "title" : title])

// PUT request
let putRequest = Request.put("boards/1", parameters: ["type" : kind, "title" : title])

// PATCH request
let patchRequest = Request.patch("boards/1", parameters: ["title" : title])

// DELETE request
let deleteRequest = Request.delete("boards/1")

URLSessionDataTask 是执行请求的默认选择。对于上传,有两种额外的选项,使用 URLSessionUploadTask 而不是 URLSessionDataTask

// Upload data to url
Request.upload(data: data, to: "boards")

// Upload multipart data with parameters
// You are responsible for constructing a proper value,
// which is normally a string created from data.
Request.upload(
  multipartParameters: ["key": "value"],
  to: "http:/api.loc/posts"
)

内容类型

  • query - 创建一个查询字符串,并将其附加到任何现有的 URL 上。
  • formURLEncoded - 将 application/x-www-form-urlencoded 用作 Content-Type,并使用百分数编码格式化参数。
  • json - 将 Content-Type 设置为 application/json 并将参数的 JSON 表示发送给请求体。
  • multipartFormData - 将参数编码为 multipart/form-data
  • custom(String) - 使用给定的 Content-Type 字符串作为头部。

编码

Malibu 提供了 3 种参数编码实现

  • FormURLEncoder - 根据 RFC 3986 标准的百分号转义编码。
  • JsonEncoder - 基于 JSONSerialization 的编码。
  • MultipartFormEncoder - 多部分数据构建器。

您可以通过添加符合 ParameterEncoding 协议的自定义参数编码器来扩展默认功能。

// Override default JSON encoder
Malibu.parameterEncoders[.json] = CustomJsonEncoder()

// Register encoder for the custom encoding type
Malibu.parameterEncoders[.custom("application/xml")] = CustomXMLEncoder()

缓存策略

URLSession 根据 URLRequest.CachePolicy 属性处理缓存。

let getRequest = Request.get("boards". cachePolicy: .useProtocolCachePolicy)

URLRequest.CachePolicy.useProtocolCachePolicy 是 URL 加载请求的默认策略。当 URLSession 获取 304 Not Modified 响应状态时,它将在发送到后端之前自动添加 If-None-Match 标头。然后,当 URLSession 获取 304 Not Modified 响应状态时,它将调用 URLSessionDataTask 完成块,并带有一致的 200 状态码和从缓存中加载的数据。

如果您想防止这种自动缓存管理,您可以设置 cachePolicy 属性为 .reloadIgnoringLocalCacheData。然后,URLSession 不会向客户端请求添加 If-None-Match 标头,服务器将始终返回完整响应。

网络

Networking 类是 Malibu 的核心组件,它实际上在每个指定的 API 服务上执行 HTTP 请求。

初始化

创建一个新的 Networking 实例十分简单。

// Simple networking that works with `SharkywatersEndpoint` requests.
let simpleNetworking = Networking<SharkywatersEndpoint>()

// More advanced networking
let networking = Networking<SharkywatersEndpoint>(
  // `OperationQueue` Mode
  mode: .async,
  // Optional mock provider
  mockProvider: customMockProvider,
  // `default`, `ephemeral`, `background` or `custom`
  sessionConfiguration: .default,
  // Custom `URLSessionDelegate` could set if needed
  sessionDelegate: self
)

模式

Malibu 使用 OperationQueue 来执行/取消请求。这使得管理请求生命周期和并发变得更加容易。

当你创建一个新的网络实例时,可以指定一个可选的 模式 参数,该参数将被使用

  • 同步
  • 异步
  • 有限(最大并发操作数:maxConcurrentOperationCount)

模拟

模拟在编写测试时非常出色。但它在后端开发者正在努力实现API时也可以加快您的开发速度。

为了开始模拟,您必须执行以下操作

创建一个模拟提供程序

// Delay is optional, 0.0 by default.
let mockProvider = MockProvider(delay: 1.0) { endpoint in
  switch endpoint {
    case .fetchBoards:
      // With response data from a file:
      return Mock(fileName: "boards.json")
    case .showBoard(let id):
      // With response from JSON dictionary:
      return Mock(json: ["id": 1, "title": "Balsa Fish"])
    case .updateBoard(let id, let title):
      // `Data` mock:
      return Mock(
        // Needed response
        response: mockedResponse,
        // Response data
        data: responseData,
        // Custom error, `nil` by default
        error: customError
      )
    default:
      return nil
  }
}

使用您的模拟提供程序创建一个网络实例

可以使用真实请求和伪造请求的混合

let networking = Networking<SharkywatersEndpoint>(mockProvider: mockProvider)

会话配置

SessionConfigurationURLSessionConfiguration 的包装器,可以表示 3 种标准会话类型 + 1 种自定义类型

  • default - 使用全局单例凭据、缓存和 cookie 存储对象的全局配置。
  • ephemeral - 不在硬盘上存储 cookie、缓存或凭据的配置。
  • background - 会话配置,可以在某些约束条件下代表挂起的应用程序执行网络操作。
  • custom(URLSessionConfiguration) - 如果您对标准类型不满意,您可以将自定义的 URLSessionConfiguration 放在这里。

预处理

// Use this closure to modify your `Request` value before `URLRequest`
// is created on base of it
networking.beforeEach = { request in
  return request.adding(
    parameters: ["userId": "12345"],
    headers: ["token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"]
  )
}

// Use this closure to modify generated `URLRequest` object
// before the request is made
networking.preProcessRequest = { (request: URLRequest) in
  var request = request
  request.addValue("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", forHTTPHeaderField: "token")
  return request
}

中间件

中间件 是在真正的请求之前的第一个 promise 函数,它可以用来准备网络,进行某种预处理任务,在特定条件下取消请求等。

例如,与 https://github.com/hyperoslo/OhMyAuth 结合使用时

// Set middleware in your configuration
// Remember to `resolve` or `reject` the promise
networking.middleware = { promise in
  AuthContainer.serviceNamed("service")?.accessToken { accessToken, error in
    if let error == error {
      promise.reject(error)
      return
    }

    guard let accessToken = accessToken else {
      promise.reject(CustomError())
      return
    }

    self.networking.authenticate(bearerToken: accessToken)
    promise.resolve()
  }
}

// Send your request like you usually do.
// Valid access token will be set to headers before the each request.
networking.request(request)
  .validate()
  .toJsonDictionary()

身份验证

// HTTP basic authentication with username and password
networking.authenticate(username: "malibu", password: "surfingparadise")

// OAuth 2.0 authentication with Bearer token
networking.authenticate(bearerToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9")

// Custom authorization header
networking.authenticate(authorizationHeader: "Malibu-Header")

发出请求

Networking 键已设置并就绪,所以是时候发送一些请求了。

let networking = Networking<SharkywatersEndpoint>()

networking.request(.fetchBoards)
  .validate()
  .toJsonDictionary()
  .done({ data in
    print(data)
  })

networking.request(.createBoard(kind: 2, title: "Balsa Fish"))
  .validate()
  .toJsonDictionary()
  .done({ data in
    print(data)
  })

networking.request(.deleteBoard(id: 11))
  .fail({ error in
    print(error)
  })

响应与 NetworkPromise

Response 对象包含 DataURLRequestHTTPURLResponse 属性。

NetworkPromise 无非是 Promise<Response> 的别名,它是由每个请求方法返回的。您可以使用 NetworkPromise 对象添加不同的回调并构建任务链。它提供了一系列有用的辅助工具,如校验和序列化。

let networkPromise = networking.request(.fetchBoards)

// Cancel the task
networkPromise.cancel()

// Create chains and add callbacks on promise object
networkPromise
  .validate()
  .toString()
  .then({ string in
    // ...
  })
  .done({ _ in
    // ...
  })

脱机存储

在没有网络连接时想存储请求吗?

let request = Request.delete(
  "boards/1",
  storePolicy: .offline // Set store policy
)

重新播放缓存请求吗?

networking.replay().done({ result
  print(result)
})

请求存储是针对网络特定的,当它重新播放缓存请求时,它将设置为 Sync 模式。缓存请求将经过正常的请求生命周期,包括应用中间件和预处理操作。请求完成时会自动从存储中移除。

Backfoot surfer

Malibu 有一个默认配置的共享网络对象,用于只需简单捕获波浪的情况。您不需要创建自定义的 RequestConvertible 类型,直接在 Malibu 上调用相同的 request 方法即可。

Malibu.request(Request.get("http://sharkywaters.com/api/boards")

响应

序列化

Malibu 提供了一系列方法来序列化响应数据

let networkPromise = networking.request(.fetchBoards)

networkPromise.toData() // -> Promise<Data>
networkPromise.toString() // -> Promise<String>
networkPromise.toJsonArray() // -> Promise<[[String: Any]]>
networkPromise.toJsonDictionary() // -> Promise<[String: Any]>

验证

Malibu 预置了4种验证方法

// Validates a status code to be within 200..<300
// Validates a response content type based on a request's "Accept" header
networking.request(.fetchBoards).validate()

// Validates a response content type
networking.request(.fetchBoards).validate(
  contentTypes: ["application/json; charset=utf-8"]
)

// Validates a status code
networking.request(.fetchBoards).validate(statusCodes: [200])

// Validates with custom validator conforming to `Validating` protocol
networking.request(.fetchBoards).validate(using: CustomValidator())

解码

Malibu 能够将响应体转换为符合 Decodable 协议的模型

// Declare your model conforming to `Decodable` protocol
struct User: Decodable {
  let name: String
  let dob: Date
}

// Set up a `JSONDecoder`
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601

// Decode your response body
networkPromise.decode(using: User.self, decoder: decoder)

日志

如果您希望在控制台查看一些请求、响应和错误信息,这是免费的。只需选择可用的日志级别之一

  • none - 禁用日志记录,因此控制台不会充斥着网络信息。
  • error - 打印执行请求过程中发生的错误。
  • info - 打印进入请求方法 + URL、响应状态码和错误。
  • verbose - 除了在 info 级别打印的内容外,还打印进入请求头和参数。

您还可以设置自己的日志记录器,并调整日志记录以满足您的需求

// Custom logger that conforms to `ErrorLogging` protocol
Malibu.logger.errorLogger = CustomErrorLogger.self

// Custom logger that conforms to `RequestLogging` protocol
Malibu.logger.requestLogger = RequestLogger.self

// Custom logger that conforms to `ResponseLogging` protocol
Malibu.logger.responseLogger = ResponseLogger.self

作者

Hyper Interaktiv AS, [email protected]

安装

Malibu 可以通过 CocoaPods 获取。要安装它,只需将以下行添加到您的 Podfile

pod 'Malibu'

Malibu 还可以通过 Carthage 获取。要安装,只需写入您的 Cartfile

github "vadymmarkov/Malibu"

Malibu 也可以手动安装。只需 下载放入 您的项目中的 /Sources 文件夹。

作者

Vadym Markov, [email protected]

鸣谢

这个库最初是在 Hyper(一家热爱优质代码和愉悦用户体验的数字通信机构)完成的。

鸣谢 Alamofire 的启发,以及 Whenpromises

贡献

有关更多信息,请查看 CONTRIBUTING 文件。

许可

Malibu 可在 MIT 许可下使用。有关更多信息,请参阅 LICENSE 文件。