缓存上下文
缓存上下文 = (请求)上下文依赖
缓存上下文类似于 HTTP 头中的 Vary。
为什么?
缓存上下文定义了如何根据上下文创建需要缓存的变体。这样编写生成缓存的代码会更易读,并且不需要在每个需要相同上下文变化的地方重复相同的逻辑。
示例:
- 某些数据的输出依赖于当前主题,不同主题会产生不同的结果。此时就会使用主题缓存上下文。
- 在生成一个渲染数组以显示个性化消息时,该渲染数组依赖于用户。此时缓存依赖于用户缓存上下文。
- 通常情况下:当某些计算开销较大的信息依赖于服务器环境时,也会使用缓存上下文。
如何?
缓存上下文是一个字符串,它引用一个可用的缓存上下文服务(见下文)。
缓存上下文以字符串集合的形式传递(顺序无关),因此它们表示为 string[]。这是集合,因为一个缓存项可能依赖于多个缓存上下文。
通常缓存上下文来自请求上下文对象(即请求)。大部分 Web 应用的环境都来自请求上下文。最终,HTTP 响应在很大程度上是根据触发它们的 HTTP 请求的属性生成的。
但这并不意味着缓存上下文必须来自请求 —— 它们也可能依赖于部署的代码,例如 deployment_id 上下文。
其次,缓存上下文以层级的形式描述。最简单的例子:当某些东西对每个用户都不同的时候,我们就不需要仅仅依赖访问权限上下文,因为对每个用户都不同。对于访问权限集,会对每个权限进行缓存。如果页面的一部分对每个用户都不同,而另一部分取决于权限,那么 Drupal 应该足够智能,只针对每个用户使用差异。这就是 Drupal 使用层级信息避免不必要缓存变体的地方。
语法
- 点号分隔父元素和子元素
- 复数形式的缓存上下文名称表示它接受参数:添加冒号,然后指定所需的参数(如果未指定参数,则会收集所有可能的参数,例如所有查询参数)
Drupal 8 核心的缓存上下文
Drupal 8 核心自带以下缓存上下文层级:
cookies
:name
headers
:name
ip
languages
:type
protocol_version // 从 8.9.x 起可用。
request_format
route
.book_navigation
.menu_active_trails
:menu_name
.name
session
.exists
theme
timezone
url
.path
.is_front // 从 8.3.x 起可用。
.parent
.query_args
:key
.pagers
:pager_id
.site
user
.is_super_user
.node_grants
:operation
.permissions
.roles
:role
注意:要在较早版本/分支中使用 url.path.is_front 缓存上下文,请参阅 变更记录。
在任何使用缓存上下文的地方,都会指定完整的层级信息,这有三个好处:
- 没有歧义:无论在何处使用,都清楚它基于哪个父级缓存上下文
- 比较(和折叠)缓存上下文变得更简单:如果同时存在 a.b.c 和 a.b,很明显 a.b 包含 a.b.c,因此可以省略 a.b.c,将其折叠到父级
- 无需保证整个树中每个层级唯一
因此,该层级中的缓存上下文示例:
- theme(依赖于当前主题)
- user.roles(依赖于角色组合)
- user.roles:anonymous(依赖于当前用户是否具有「匿名」角色,即是否为匿名用户)
- languages(针对所有语言类型:界面、内容…)
- languages:language_interface(依赖于界面语言 - LanguageInterface::TYPE_INTERFACE)
- languages:language_content(依赖于内容语言 - LanguageInterface::TYPE_CONTENT)
- url(依赖于完整 URL)
- url.query_args(依赖于整个查询字符串)
- url.query_args:foo(依赖于查询参数 ?foo)
- protocol_version(依赖于 HTTP 1 或 2)
优化/折叠/合并缓存上下文
Drupal 会自动使用层级信息来尽量简化缓存上下文。例如,当页面的一部分根据用户变化(缓存上下文 user),另一部分根据权限变化(缓存上下文 user.permissions)时,最终结果不需要再在权限上变化,因为对每个用户已经不同了。
换句话说:optimize([user, user.permissions]) = [user]。
即使开发者确实希望表示 user.permissions,但在优化后,权限的任何更改不会再导致 user.permissions 缓存上下文加载到每个页面。这意味着如果权限发生变化,我们仍然会继续使用相同的缓存版本,即使它本应在权限更改时更新。
因此,那些依赖于可能随时间改变的配置的缓存上下文,可以通过绑定缓存元数据来解决:缓存标签和最大存活时间 (max-age)。当这样的缓存上下文被优化时,它的缓存标签会绑定到缓存项。因此,每当权限发生更改时,该缓存项也会失效。
类似但更复杂的例子是节点授权 (node grants)。节点授权是针对具体用户的,因此缓存上下文是 user.node_grants。但节点授权可能极其动态(例如依赖时间,每几分钟就会变化)。这取决于站点上实现的 node grant hook。因此更适合为其使用 max-age = 0 的缓存上下文,表示它不可缓存(即不可优化)。因此,optimize([user, user.node_grants]) = [user, user.node_grants]。
某些站点可能会覆盖默认的节点授权缓存上下文实现,并改为指定 max-age = 3600,表示它们的 node grant hook 允许缓存授权结果最多一小时。在这样的站点上,optimize([user, user.node_grants]) = [user]。
如何识别、定义和创建?
缓存上下文是带有 cache.context 标签的服务。因此任何模块都可以添加缓存上下文。它们实现 \Drupal\Core\Cache\Context\CacheContextInterface 或 \Drupal\Core\Cache\Context\CalculatedCacheContextInterface(用于接受参数的缓存上下文,即带有 :parameter 后缀的缓存上下文)。
因此,要找到所有可用的缓存上下文,只需查看 CacheContextInterface 和 CalculatedCacheContextInterface,并使用 IDE 查找所有实现。(在 PHPStorm 中:类型层级 → 子类型层级;在 NetBeans 中:右键点击接口名称 → 查找用法 → 查找所有子类型。)
或者,你可以使用 Drupal 控制台 (drupal debug:cache:context) 来显示你网站或应用的所有缓存上下文:
$ drupal debug:cache:context Context ID Label Class path cookies HTTP-Cookies Drupal\Core\Cache\Context\CookiesCacheContext headers HTTP-Header Drupal\Core\Cache\Context\HeadersCacheContext ip IP-Adresse Drupal\Core\Cache\Context\IpCacheContext languages Language Drupal\Core\Cache\Context\LanguagesCacheContext request_format Anfrageformat Drupal\Core\Cache\Context\RequestFormatCacheContext route Route Drupal\Core\Cache\Context\RouteCacheContext route.book_navigation Buchnavigation Drupal\book\Cache\BookNavigationCacheContext route.menu_active_trails Aktiver Menüpfad Drupal\Core\Cache\Context\MenuActiveTrailsCacheContext
在每个找到的类中,你会看到类似的注释 \Drupal\Core\Cache\Context\UserCacheContext:
Cache context ID: 'user'.
这意味着 'user' 就是你可以在代码中指定的实际缓存上下文。(或者查看该类在 *.services.yml 文件中的使用位置,查看服务 ID。)
提示:你可以通过查看所有带有 cache_context 标签的服务,快速获取 Drupal 核心中的完整缓存上下文列表!
服务 ID 是标准化的。它总是以 cache_context. 开头,后面跟着父级缓存上下文,最后是缓存上下文的名称。例如:cache_context(必需前缀)+ route(父级)+ book_navigation(该缓存上下文的名称):
cache_context.route.book_navigation:
class: Drupal\book\Cache\BookNavigationCacheContext
arguments: ['@request_stack']
tags:
- { name: cache.context }
这定义了缓存上下文 route.book_navigation。
调试
以上所有内容在调试缓存时都非常有用。但还有一件事:假设某个东西使用缓存键 ['foo', 'bar'] 和缓存上下文 ['languages:language_interface', 'user.permissions', 'route'] 进行缓存。那么相应的缓存项会存储在一个缓存容器中,CID(缓存 ID)如下:
foo:bar:[languages:language_interface]=en:[user.permissions]=A_QUITE_LONG_HASH:[route]=myroute.ROUTE_PARAMS_HASH
换句话说:
- 缓存键首先列出,按指定顺序
- 缓存上下文其次按字母顺序列出,并生成 [<缓存上下文名称>]=<缓存上下文值> 形式的 CID 片段
- 所有这些 CID 片段通过冒号组合
这会让分析和调试缓存更容易!
响应头(调试)
最后:很容易看到某个响应依赖于哪些缓存上下文(因此它会发生变化):只需查看 X-Drupal-Cache-Contexts 响应头!
注意:如果你没有看到这些头,需要 配置 Drupal 开发环境。
动态页面缓存
Drupal 8 中对缓存上下文的广泛使用,使其默认启用了 动态页面缓存。(之前称为「Smart Cache」)
内部页面缓存
注意:内部页面缓存 假设所有匿名用户看到的页面都是相同的,而不管缓存上下文的实现。如果你想对匿名用户提供基于缓存上下文的不同内容,则必须禁用此模块,这可能会影响性能。