跳转至

内容

基于 Vapor 的 content API,你可以轻松地对 HTTP 消息中的可编码结构进行编码/解码。默认使用 JSON 编码,并支持 URL-Encoded FormMultipart。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 类型的 beforeDecodeafterDecode。提供了默认的实现,但你可以使用这些方法来自定义逻辑实现:

// 在此内容被解码后运行。
// 此内容解码后运行。只有 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 消息体中内容的编码器指定了两种协议:ContentDecoderContentEncoder

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 查询字符串中的内容的编码器指定了两个协议: URLQueryDecoderURLQueryEncoder

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>
  """)
}