内容协商
什么是内容协商?
内容协商是一种机制,用于根据客户端的处理能力以及服务器的服务能力,来决定向客户端返回何种类型的内容。这种机制可用于确定客户端是希望接收 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'。
所以,选择区域设置的过程如下:
精确匹配('en-GB')- ISO 639-1 加上 ISO 3166-1 alpha-2
通用区域匹配('en')- ISO 639-1
区域性匹配('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']);