# 内容协商

## 什么是内容协商？

内容协商是一种机制，用于根据客户端的处理能力以及服务器的服务能力，来决定向客户端返回何种类型的内容。这种机制可用于确定客户端是希望接收 HTML 还是 JSON 数据，图片是应该以 JPEG 还是 PNG 格式返回，支持哪种类型的压缩方式等等。这一过程通过分析四个不同的标头（Header）信息来实现，每个标头都支持多个选项，且各自拥有不同的优先级。

手动去匹配这些信息可能颇具挑战性。CodeIgniter 提供了 `Negotiator` 类来为你自动处理这些工作。

本质上，内容协商只是 HTTP 规范的一部分，它允许单个资源提供多种类型的内容，从而让客户端能够请求最适合它们的数据类型。

一个经典的例子是：如果浏览器无法显示 PNG 文件，它可能会请求只接收 GIF 或 JPEG 图片。当服务器收到请求时，它会查看客户端请求的可用文件类型，并从服务器支持的图片格式中选择最佳匹配项——在这种情况下，很可能会选择返回 JPEG 图片。

同样的协商机制也适用于以下四种类型的数据：

- 媒体/文档类型 - 可能是图片格式，或者是 HTML 与 XML 或 JSON 的选择。

- 字符集 - 返回文档所使用的字符集。通常是 UTF-8。

- 文档编码 - 通常指对结果使用的压缩类型。

- 文档语言 - 对于支持多语言的站点，这有助于确定返回哪种语言的内容。

## 加载类

你可以通过 Service 类手动加载该类的实例：

    <?php

    $negotiate = service('negotiator');

这将获取当前的请求实例，并将其自动注入到 Negotiator 类中。

这个类不需要单独加载。相反，可以通过当前请求的 `IncomingRequest` 实例来访问。虽然不能直接通过这种方式访问该类本身，但你可以通过 `negotiate()` 方法轻松访问其所有功能：

    <?php

    $request->negotiate('media', ['foo', 'bar']);

当通过这种方式访问时，第一个参数是你试图寻找匹配的内容类型，第二个参数是支持的值的数组。

## 协商

在本节中，我们将讨论可以协商的 4 种内容类型，并展示如何使用上述两种访问 Negotiator 的方法来实现协商。

### 媒体

首先要看的是处理“媒体”协商。这由 `Accept` 标头提供，它是可用的最复杂的标头之一。一个常见的例子是客户端告诉服务器它希望数据以何种格式返回。这在 API 中尤为常见。例如，客户端可能会从 API 端点请求 JSON 格式的数据:

    GET /foo HTTP/1.1
    Accept: application/json

此时服务器需要提供一份它能提供的内容类型列表。在这个例子中，API 可能能够以原始 HTML、JSON 或 XML 格式返回数据。该列表应按优先顺序提供：

    <?php

    $supported = [
        'application/json',
        'text/html',
        'application/xml',
    ];

    $format = $request->negotiate('media', $supported);
    // or
    $format = $negotiate->media($supported);

在这种情况下，客户端和服务器都同意将数据格式化为 JSON，因此 negotiate 方法会返回“json”。默认情况下，如果没有找到匹配项，将返回 `$supported` 数组中的第一个元素。但在某些情况下，你可能需要强制格式进行严格匹配。如果将 `true` 作为最后一个参数传递，当未找到匹配项时，它将返回一个空字符串：

    <?php

    $format = $request->negotiate('media', $supported, true);
    // or
    $format = $negotiate->media($supported, true);

### 语言

另一个常见的用途是确定内容应以何种语言提供。如果你运行的是单语言站点，这显然没什么区别；但对于任何提供多语言翻译内容的站点来说，这非常有用，因为浏览器通常会在 `Accept-Language` 标头中发送首选语言:

    GET /foo HTTP/1.1
    Accept-Language: fr; q=1.0, en; q=0.5

在这个例子中，浏览器首选法语，其次是英语。如果你的网站支持英语和德语，你可以这样做：

    <?php

    $supported = [
        'en',
        'de',
    ];

    $lang = $request->negotiate('language', $supported);
    // or
    $lang = $negotiate->language($supported);

在这个例子中，将返回 'en' 作为当前语言。如果没有找到匹配项，它将返回 `$supported` 数组中的第一个元素，因此该元素应始终设为你的首选语言/默认语言。

#### 严格的区域协商

Added in version 4.6.0.

默认情况下，区域设置（Locale）是基于一种宽泛（非精确）的比较机制来确定的。因此，只有区域设置字符串的第一部分（即语言）会被纳入考量。通常这已经足够了。但有时我们希望能区分区域版本，例如 `en-US` 和 `en-GB`，以提供不同的内容。

针对这种情况，我们引入了一个新的设置，可以通过 `Config\Feature::$strictLocaleNegotiation` 启用。这将确保系统优先进行严格比较。

备注

CodeIgniter 仅附带主语言标签（如 'en', 'fr' 等）的翻译文件。因此，如果你启用了此功能，并且你的 `Config\App::$supportedLocales` 设置中包含了区域语言标签（如 'en-US', 'fr-FR' 等），请务必记住：如果你有自己的翻译文件，你 必须同时更改 CodeIgniter 翻译文件的文件夹名称，以匹配你在 `$supportedLocales` 数组中设置的名称。

现在让我们看看下面的例子。浏览器的首选语言设置如下:

    GET /foo HTTP/1.1
    Accept-Language: fr; q=1.0, en-GB; q=0.5

在这个例子中，浏览器首选法语，其次是英语（英国）。而你的网站支持德语和英语（美国）：

    <?php

    $supported = [
        'de',
        'en-US',
    ];

    $lang = $request->negotiate('language', $supported);
    // or
    $lang = $negotiate->language($supported);

在此例中，将返回 'en-US' 作为当前语言。如果没有找到匹配项，它将返回 `$supported` 数组中的第一个元素。以下是区域选择过程的具体工作原理：

尽管浏览器首选 'fr'，但它不在我们的 `$supported` 数组中。'en-GB' 也遇到了同样的问题，但在这里我们可以搜索变体。首先，我们会回退到最通用的区域设置（在本例中为 'en'），但这也不在我们的数组中。然后，我们将搜索区域设置 'en-'。这时，我们 `$supported` 数组中的值就会被匹配到。最终我们将返回 'en-US'。

所以，选择区域设置的过程如下：

1. 精确匹配（'en-GB'）- ISO 639-1 加上 ISO 3166-1 alpha-2

2. 通用区域匹配（'en'）- ISO 639-1

3. 区域性匹配（'en-'）- ISO 639-1 加上针对 ISO 3166-1 alpha-2 的“通配符”

### 编码

`Accept-Encoding` 标头包含了客户端首选接收的字符集（注：此处原文可能有误，通常指编码方式），用于指定客户端支持的压缩类型:

    GET /foo HTTP/1.1
    Accept-Encoding: compress, gzip

你的 Web 服务器将定义你可以使用哪种类型的压缩。有些服务器（如 Apache）仅支持 gzip：

    <?php

    $type = $request->negotiate('encoding', ['gzip']);
    // or
    $type = $negotiate->encoding(['gzip']);

更多信息请参阅 维基百科。

### 字符集

期望的字符集通过 `Accept-Charset` 标头传递:

    GET /foo HTTP/1.1
    Accept-Charset: utf-16, utf-8

默认情况下，如果未找到匹配项，将返回 utf-8：

    <?php

    $charset = $request->negotiate('charset', ['utf-8']);
    // or
    $charset = $negotiate->charset(['utf-8']);