内容¶
基于 Vapor 的 content API,你可以轻松地对 HTTP 消息中的可编码结构进行编码/解码。默认使用 JSON 编码,并支持 URL-Encoded Form 和 Multipart。Content API 可以灵活配置,允许你为某些 HTTP 请求类型添加、修改或替换编码策略。
概述¶
要了解 Vapor 的 content API 是如何工作的,你应该先了解一些关于 HTTP 的基础知识。 看看下面这个请求的示例:
POST /greeting HTTP/1.1
content-type: application/json
content-length: 18
{"hello": "world"}
该请求表明,它包含使用 content-type
标头和 application/json
媒体类型的 JSON 编码数据。如前所述,JSON 数据在正文中的标头之后。
内容结构¶
解码此 HTTP 消息的第一步是创建匹配预期结构的可编码类型。
struct Greeting: Content {
var hello: String
}
使上面的 Greeting
数据类型遵循 Content
协议,将同时支持 Codable
协议规则,符合 Content API 的其他程序代码。
然后就可以使用 req.content
从传入的请求中对数据进行解码,如下所示:
app.post("greeting") { req in
let greeting = try req.content.decode(Greeting.self)
print(greeting.hello) // "world"
return HTTPStatus.ok
}
解码方法使用请求的 content 类型来寻找合适的解码器,如果没有找到解码器,或者请求中不包含 content 类型标头,将抛出 415
错误。
这意味着该路由自动接受所有其他支持的内容类型,如 url-encoded form 形式:
POST /greeting HTTP/1.1
content-type: application/x-www-form-urlencoded
content-length: 11
hello=world
在文件上传的情况下,你的内容属性必须是 Data
类型。
struct Profile: Content {
var name: String
var email: String
var image: Data
}
支持的媒体类型¶
以下是 content API 默认支持的媒体类型:
name | header value | media type |
---|---|---|
JSON | application/json | .json |
Multipart | multipart/form-data | .formData |
URL-Encoded Form | application/x-www-form-urlencoded | .urlEncodedForm |
Plaintext | text/plain | .plainText |
HTML | text/html | .html |
不是所有的媒体类型都支持所有的 Codable
协议。例如,JSON 不支持顶层片段,Plaintext 不支持嵌套数据。
查询(Query)¶
Vapor 的 Content API 支持处理 URL 查询字符串中的 URL 编码数据。
解码¶
要了解 URL 查询字符串的解码是如何工作的,请看下面的示例请求:
GET /hello?name=Vapor HTTP/1.1
content-length: 0
就像处理 HTTP 消息正文内容的 API 一样,解析 URL 查询字符串的第一步是创建一个与预期结构相匹配的 struct
。
struct Hello: Content {
var name: String?
}
注意 name
是一个可选的 String
,因为 URL 查询字符串应该是可选的。如果你需要一个参数,请用路由参数代替。
现在,你已经为该路由的预期查询字符串提供了 Content
结构,可以对其进行解码了。
app.get("hello") { req -> String in
let hello = try req.query.decode(Hello.self)
return "Hello, \(hello.name ?? "Anonymous")"
}
给定上面的请求,此路由将触发以下响应:
HTTP/1.1 200 OK
content-length: 12
Hello, Vapor
如果省略了查询字符串,如以下请求中所示,将使用"匿名"来代替。
GET /hello HTTP/1.1
content-length: 0
单值¶
除了对 Content
结构进行解码外,Vapor 还支持使用下标从查询字符串中获取单个参数值。
let name: String? = req.query["name"]
钩子¶
Vapor 会自动调用 Content
类型的 beforeDecode
和 afterDecode
。提供了默认的实现,但你可以使用这些方法来自定义逻辑实现:
// 在此内容被解码后运行。
// 此内容解码后运行。只有 Struct 才需要 'mutating',而 Class 则不需要。
mutating func afterDecode() throws {
// 名称可能没有传入,但如果传入了,那就不能是空字符串。
self.name = self.name?.trimmingCharacters(in: .whitespacesAndNewlines)
if let name = self.name, name.isEmpty {
throw Abort(.badRequest, reason: "Name must not be empty.")
}
}
// 在对该内容进行编码之前运行。只有 Struct 才需要 'mutating',而 Class 则不需要。
mutating func beforeEncode() throws {
// 必须*总是*传递一个名称回来,它不能是一个空字符串。
guard
let name = self.name?.trimmingCharacters(in: .whitespacesAndNewlines),
!name.isEmpty
else {
throw Abort(.badRequest, reason: "Name must not be empty.")
}
self.name = name
}
覆盖默认值¶
可以配置 Vapor 的 Content API 所使用的默认编码器和解码器。
全局¶
ContentConfiguration.global
允许你修改 Vapor 默认使用的编码器和解码器。这对于改变整个应用程序的数据解析和序列化方式非常有用。
// 创建一个新的 JSON 编码器,使用 unix-timestamp 日期编码
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .secondsSince1970
// 覆盖用于媒体类型 `.json` 的全局编码器。
ContentConfiguration.global.use(encoder: encoder, for: .json)
通常是在 configure.swift
文件中修改 ContentConfiguration
。
单次生效¶
对编码和解码方法的调用,如 req.content.decode
,支持为单次使用配置自定义编码器。
// 创建一个新的 JSON 解码器,使用 unix-timestamp 日期的时间戳
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
// 使用自定义解码器对 `Hello` 结构进行解码
let hello = try req.content.decode(Hello.self, using: decoder)
定制编码器¶
应用程序和第三方软件包可以通过创建自定义编码器,对 Vapor 默认不支持的媒体类型进行扩展支持。
内容¶
Vapor 为能够处理 HTTP 消息体中内容的编码器指定了两种协议:ContentDecoder
和 ContentEncoder
。
public protocol ContentEncoder {
func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders) throws
where E: Encodable
}
public protocol ContentDecoder {
func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D
where D: Decodable
}
遵循这些协议,允许你的自定义编码器注册到上面指定的 ContentConfiguration
。
URL 查询¶
Vapor 为能够处理 URL 查询字符串中的内容的编码器指定了两个协议: URLQueryDecoder
和 URLQueryEncoder
。
public protocol URLQueryDecoder {
func decode<D>(_ decodable: D.Type, from url: URI) throws -> D
where D: Decodable
}
public protocol URLQueryEncoder {
func encode<E>(_ encodable: E, to url: inout URI) throws
where E: Encodable
}
遵循这些协议,可以将你的自定义编码器注册到 ContentConfiguration
中,以使用 use(urlEncoder:)
和 use(urlDecoder:)
方法处理 URL 查询字符串。
自定义 ResponseEncodable
¶
另一种方法涉及到在你的类型上实现 ResponseEncodable
,请看下面这个 HTML
包装类型。
struct HTML {
let value: String
}
它的 ResponseEncodable
实现看起来像这样:
extension HTML: ResponseEncodable {
public func encodeResponse(for request: Request) -> EventLoopFuture<Response> {
var headers = HTTPHeaders()
headers.add(name: .contentType, value: "text/html")
return request.eventLoop.makeSucceededFuture(.init(
status: .ok, headers: headers, body: .init(string: value)
))
}
}
如果你正在使用 async
/await
你可以使用 AsyncResponseEncodable
:
extension HTML: AsyncResponseEncodable {
public func encodeResponse(for request: Request) async throws -> Response {
var headers = HTTPHeaders()
headers.add(name: .contentType, value: "text/html")
return .init(status: .ok, headers: headers, body: .init(string: value))
}
}
注意,它允许自定义 Content-Type
头,查看更多请查阅 HTTPHeaders
文档
接下来,你可以在你的路由中使用 HTML
作为响应类型:
app.get { _ in
HTML(value: """
<html>
<body>
<h1>Hello, World!</h1>
</body>
</html>
""")
}