# 内容

基于 Vapor 的 content API，你可以轻松地对 HTTP 消息中的可编码结构进行编码/解码。默认使用 [JSON](https://tools.ietf.org/html/rfc7159) 编码，并支持 [URL-Encoded Form](https://en.wikipedia.org/wiki/Percent-encoding#The_application/x-www-form-urlencoded_type) 和 [Multipart](https://tools.ietf.org/html/rfc2388)。Content API 可以灵活配置，允许你为某些 HTTP 请求类型添加、修改或替换编码策略。


## 概述

要了解 Vapor 的 content API 是如何工作的，你应该先了解一些关于 HTTP 的基础知识。
看看下面这个请求的示例：

```http
POST /greeting HTTP/1.1
content-type: application/json
content-length: 18

{"hello": "world"}
```

该请求表明，它包含使用 `content-type` 标头和 `application/json` 媒体类型的 JSON 编码数据。如前所述，JSON 数据在正文中的标头之后。

### 内容结构

解码此 HTTP 消息的第一步是创建匹配预期结构的可编码类型。

```swift
struct Greeting: Content {
    var hello: String
}
```

使上面的 `Greeting` 数据类型遵循 `Content` 协议，将同时支持 `Codable` 协议规则，符合 Content API 的其他程序代码。

然后就可以使用 `req.content` 从传入的请求中对数据进行解码，如下所示：

```swift
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 形式:

```http
POST /greeting HTTP/1.1
content-type: application/x-www-form-urlencoded
content-length: 11

hello=world
```

在文件上传的情况下，你的内容属性必须是 `Data` 类型。

```swift
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 查询字符串的解码是如何工作的，请看下面的示例请求：

```http
GET /hello?name=Vapor HTTP/1.1
content-length: 0
```

就像处理 HTTP 消息正文内容的 API 一样，解析 URL 查询字符串的第一步是创建一个与预期结构相匹配的 `struct` 。

```swift
struct Hello: Content {
    var name: String?
}
```

注意 `name` 是一个可选的 `String`，因为 URL 查询字符串应该是可选的。如果你需要一个参数，请用路由参数代替。

现在，你已经为该路由的预期查询字符串提供了 `Content` 结构，可以对其进行解码了。

```swift
app.get("hello") { req -> String in 
    let hello = try req.query.decode(Hello.self)
    return "Hello, \(hello.name ?? "Anonymous")"
}
```

给定上面的请求，此路由将触发以下响应：

```http
HTTP/1.1 200 OK
content-length: 12

Hello, Vapor
```

如果省略了查询字符串，如以下请求中所示，将使用"匿名"来代替。

```http
GET /hello HTTP/1.1
content-length: 0
```

### 单值

除了对 `Content` 结构进行解码外，Vapor 还支持使用下标从查询字符串中获取单个参数值。

```swift
let name: String? = req.query["name"]
```

## 钩子

Vapor 会自动调用 `Content` 类型的 `beforeDecode` 和 `afterDecode`。提供了默认的实现，但你可以使用这些方法来自定义逻辑实现：

```swift
// 在此内容被解码后运行。
// 此内容解码后运行。只有 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 默认使用的编码器和解码器。这对于改变整个应用程序的数据解析和序列化方式非常有用。

```swift
// 创建一个新的 JSON 编码器，使用 unix-timestamp 日期编码
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .secondsSince1970

// 覆盖用于媒体类型 `.json` 的全局编码器。
ContentConfiguration.global.use(encoder: encoder, for: .json)
```

通常是在 `configure.swift` 文件中修改 `ContentConfiguration`。

### 单次生效

对编码和解码方法的调用，如 `req.content.decode` ，支持为单次使用配置自定义编码器。

```swift
// 创建一个新的 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`。

```swift
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`。

```swift
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` 包装类型。

```swift
struct HTML {
  let value: String
}
```

它的 `ResponseEncodable` 实现看起来像这样：

```swift
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`：

```swift
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` 文档](https://api.vapor.codes/vapor/documentation/vapor/response/headers)

接下来，你可以在你的路由中使用 `HTML` 作为响应类型：

```swift
app.get { _ in
  HTML(value: """
  <html>
    <body>
      <h1>Hello, World!</h1>
    </body>
  </html>
  """)
}
```
