logo

额外区块类型 (EBT) - 全新的布局构建器体验❗

额外区块类型 (EBT) - 样式化、可定制的区块类型:幻灯片、标签页、卡片、手风琴等更多类型。内置背景、DOM Box、JavaScript 插件的设置。立即体验布局构建的未来。

演示 EBT 模块 下载 EBT 模块

❗额外段落类型 (EPT) - 全新的 Paragraphs 体验

额外段落类型 (EPT) - 类似的基于 Paragraph 的模块集合。

演示 EPT 模块 滚动

滚动
03/10/2025, by Ivan
缓存上下文 = (请求)上下文依赖
缓存上下文类似于 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」)

内部页面缓存

注意:内部页面缓存 假设所有匿名用户看到的页面都是相同的,而不管缓存上下文的实现。如果你想对匿名用户提供基于缓存上下文的不同内容,则必须禁用此模块,这可能会影响性能。

另请参阅