描述
棕榈树、珊瑚礁和冲浪的海浪。欢迎来到冲浪俱乐部 Malibu,这是一个基于 promises 构建的网络库。它不仅仅是围绕 URLSession
的包装,而是一个强大的框架,帮助您链接请求、验证和处理请求。
在底层使用 When,Malibu 增加了很多辅助函数,并将您的代码提升到下一个层次
- 不再有“回调地狱”。
- 您的请求在同一个地方描述。
- 响应处理可以轻松分解为多个逻辑任务。
- 数据和错误分别处理。
- 您的网络代码更加干净、易于阅读,并遵循
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-Language
、Accept-Encoding
和 User-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)
会话配置
SessionConfiguration
是 URLSessionConfiguration
的包装器,可以表示 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
对象包含 Data
、URLRequest
和 HTTPURLResponse
属性。
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 的启发,以及 When 的 promises。
贡献
有关更多信息,请查看 CONTRIBUTING 文件。
许可
Malibu 可在 MIT 许可下使用。有关更多信息,请参阅 LICENSE 文件。