Yii 2 HTTP 缓存实战:用 HttpCache 过滤器驾驭 Last-Modified、ETag 与 Cache-Control
2026/9/24 14:09:15 网站建设 项目流程
  • 后端
  • Web框架

【免费下载链接】yii2

Yii 2: The Fast, Secure and Professional PHP Framework

项目地址:https://gitcode.com/gh_mirrors/yi/yii2
点击查看免费下载

本篇技术指南围绕 Yii 2 框架的客户端缓存能力展开:通过配置yii\filters\HttpCache动作过滤器,为控制器操作渲染的页面设置Last-ModifiedETagCache-Control三类 HTTP 缓存头,从而让浏览器等客户端直接复用已缓存内容,节省服务端渲染与页面传输的开销。读完本文,你将掌握 HttpCache 的完整配置方法、底层缓存校验流程(含 304 响应原理),以及 Session 与缓存头冲突的解决方案,可直接在真实项目中落地客户端缓存策略。

从服务器端缓存到客户端缓存

在前面的章节(参见 缓存概览)中介绍的数据缓存、片段缓存、页面缓存都属于服务器端缓存:缓存内容存放在服务端(文件、内存、数据库等),下次请求时由服务端直接读取缓存结果,省去的是重复计算与查询。

HTTP 缓存(客户端缓存)走的是另一条路:把「页面是否变化」的判定权交给客户端。服务器通过 HTTP 响应头告诉浏览器页面最后修改时间或内容指纹,浏览器在后续请求中携带条件请求头(If-Modified-Since/If-None-Match),若内容未变化,服务器直接返回304 Not Modified(不带响应体),浏览器继续使用本地缓存。这样服务端既不需要重新渲染页面,也不需要在网络上重新传输页面内容,两方面的开销同时被省去。

在 Yii 2 中,实现这一机制的组件就是yii\filters\HttpCache。它本质上是一个动作过滤器(ActionFilter),挂在控制器的behaviors()中即可生效,仅对GETHEAD请求起作用,并能处理三种缓存相关的 HTTP 头:

  • Last-Modified—— 页面最后修改时间戳
  • ETag—— 页面内容的实体标签(哈希指纹)
  • Cache-Control—— 页面常规缓存策略

启用 HttpCache 过滤器

在控制器中声明行为即可启用:

public function behaviors() { return [ [ 'class' => 'yii\filters\HttpCache', 'only' => ['index'], 'lastModified' => function ($action, $params) { $q = new \yii\db\Query(); return $q->from('post')->max('updated_at'); }, ], ]; }

这里用到的是ActionFilter提供的两个通用属性(实现见 framework/base/ActionFilter.php):

  • only:过滤器仅作用于列出的动作 ID。不设置则作用于所有动作。
  • except:排除列表。若某动作同时出现在onlyexcept中,则不应用该过滤器。自 2.0.9 起动作 ID 支持通配符,如site/*

HttpCache自身还有一个enabled属性(framework/filters/HttpCache.php#L111),默认为true,可在运行时(如调试模式、后台任务)动态关闭缓存。

Last-Modified头:基于时间戳的缓存校验

Last-Modified用时间戳标明页面自上次客户端缓存以来是否被修改。配置lastModified属性(值应为 PHP callable)即可开启:

/** * @param Action $action 当前处理的动作对象 * @param array $params “params” 属性的值 * @return int 页面修改时的 Unix 时间戳 */ function ($action, $params)

典型用法是查询数据表的updated_at最大值,作为页面「最新修改时间」:

public function behaviors() { return [ [ 'class' => 'yii\filters\HttpCache', 'only' => ['index'], 'lastModified' => function ($action, $params) { $q = new \yii\db\Query(); return $q->from('post')->max('updated_at'); }, ], ]; }

工作流程:上述代码表明 HTTP 缓存只在index操作启用,服务器基于帖子最后更新时间生成Last-Modified头。浏览器第一次访问index页时,服务器正常生成页面并发送;此后只要期间没有帖子被修改,浏览器再次访问时服务器不会重新生成页面,而是返回 304,浏览器直接使用客户端缓存的版本。

从源码看,beforeAction()中会把这个时间戳格式化为标准的 HTTP 日期格式(framework/filters/HttpCache.php#L148-L150):

$response->getHeaders()->set('Last-Modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');

回调返回null时不会发送该头,也不会产生 304 响应(对应 CHANGELOG 中 "no longer returns 304 HTTP code when callbacks return null" 的修复)。

ETag头:基于内容指纹的缓存校验

"Entity Tag"(实体标签)用哈希值表示页面内容,页面一旦修改哈希即改变。浏览器对比自己保存的哈希与服务器端生成的哈希,即可判断页面是否变化。配置etagSeed属性(值为 PHP callable,返回用于生成哈希的种子字符串)开启:

/** * @param Action $action 当前处理的动作对象 * @param array $params “params” 属性的值 * @return string 一段种子字符用来生成 ETag 哈希值 */ function ($action, $params)

示例——基于文章标题和内容生成 ETag:

public function behaviors() { return [ [ 'class' => 'yii\filters\HttpCache', 'only' => ['view'], 'etagSeed' => function ($action, $params) { $post = $this->findModel(\Yii::$app->request->get('id')); return serialize([$post->title, $post->content]); }, ], ]; }

工作流程:HTTP 缓存只在view操作启用,服务器根据所请求文章的标题与内容生成ETag头。首次访问正常渲染;此后只要文章标题、内容未变,服务器不重新生成页面,浏览器使用缓存内容。

ETag 生成算法与弱校验

HttpCache内部将种子通过 SHA-1 生成强 ETag(framework/filters/HttpCache.php#L209-L213):

protected function generateEtag($seed) { $etag = '"' . rtrim(base64_encode(sha1($seed, true)), '=') . '"'; return $this->weakEtag ? 'W/' . $etag : $etag; }
  • 默认生成强 ETag("…"),表示内容字节级一致;
  • 自 2.0.8 起可配置weakEtag = true生成弱 ETag(W/"…"),适用于内容语义等价但不必字节一致(如 gzip 压缩差异)的场景。HttpCacheTesttestGenerateEtag验证了强/弱 ETag 的前后缀格式(tests/framework/filters/HttpCacheTest.php#L91-L125)。

选择简单、高效的种子表达式

ETag 相比Last-Modified能实现更复杂、更精确的缓存策略。例如当站点切换主题时,可以在种子里加入主题标识使所有 ETag 失效。但注意:每次请求都必须重新计算 ETag,复杂的种子生成逻辑会违背使用HttpCache的初衷,带来不必要的性能开销。请尽量寻找一个能触发失效的最简表达式——比如上面示例中的serialize([$post->title, $post->content]),就是一条低成本 SQL 加一次序列化。

RFC 7232 关于两种头并存的规定

Note: 为了遵循 RFC 7232(HTTP 1.1 协议),如果同时配置了ETagLast-Modified头,HttpCache将会同时发送它们。并且如果客户端同时发送If-None-Match头和If-Modified-Since头,则只有前者会被接受。

这一规则在validateCache()中落实(framework/filters/HttpCache.php#L167-L178):If-None-Match的优先级高于If-Modified-Since,且支持多个 ETag 的比对——Request::getETags()会解析请求头中的多个 ETag(framework/web/Request.php#L1711-L1718)。HttpCacheTest::testValidateCache对两种头的优先级、精确匹配、通配符*等情况均有覆盖(tests/framework/filters/HttpCacheTest.php#L54-L86)。

Cache-Control头:定义常规缓存策略

Cache-Control头指定页面的通用缓存策略(如是否允许共享缓存、缓存有效期),通过配置cacheControlHeader属性发送,默认值为:

Cache-Control: public, max-age=3600

即「允许公共缓存,有效期 1 小时」。可按需覆盖,例如不希望 CDN 或代理缓存、只想让浏览器私有缓存:

[ 'class' => 'yii\filters\HttpCache', 'cacheControlHeader' => 'private, max-age=600', ]

注意该属性默认是字符串而非null,因此每次请求都会发送Cache-Control头;若设置为null则不发送(见 framework/filters/HttpCache.php#L96 与sendCacheControlHeader()的实现 framework/filters/HttpCache.php#L184-L202)。

会话缓存限制器:解决与 Session 的冲突

当页面使用 Session 时,PHP 会按照php.inisession.cache_limiter的设置自动发送一些缓存相关 HTTP 头(如ExpiresCache-ControlPragmaLast-Modified)。这些头可能干扰甚至使你的HttpCache失效——比如默认的nocache限制器会发送禁止缓存的头,导致Cache-Control: public无法生效。

默认行为HttpCachesessionCacheLimiter属性默认为空字符串'',表示完全禁止 PHP 自动发送这些缓存头(源码中通过header_remove()移除ExpiresCache-ControlLast-ModifiedPragma实现,framework/filters/HttpCache.php#L186-L195)。

自定义行为:若想改变,可将该属性设为以下字符串值之一:

取值含义(依据 PHP 手册session_cache_limiter()
public允许公共(共享)缓存,适合公开内容
private仅允许浏览器私有缓存,代理不可缓存
private_no_expireprivate,但不发送Expires
nocache禁止一切缓存

若将该属性设为null,则HttpCache完全不管 Session 缓存头,PHP 按session.cache_limiter配置自行发送。参考 PHP 手册 缓存限制器 了解各值的详细含义。

缓存校验的完整流程(源码级)

综合 framework/filters/HttpCache.php#L117-L157 的beforeAction()实现,一次带缓存的请求处理如下:

  1. 前置检查enabledfalse、请求方法不是GET/HEAD、或lastModifiedetagSeed均为null时,直接放行,不做任何缓存处理;
  2. 计算缓存验证信息:依次调用lastModified回调获得时间戳、调用etagSeed回调获得种子(null时跳过),再经generateEtag()生成 ETag;
  3. 发送Cache-Control,并按需处理 Session 缓存限制器;
  4. 发送ETag(若生成了);
  5. 校验缓存有效性validateCache()):优先看If-None-Match(与请求携带的 ETag 集合逐一精确比较),其次看If-Modified-Since(与Last-Modified时间戳比较);只有当Last-Modified存在且缓存有效但无 ETag 时才发送Last-Modified头(符合 RFC 7232 第 4.1 节);
  6. 命中缓存:设置状态码304并返回false终止后续动作执行——控制器动作、视图渲染、响应体传输全部跳过。Response组件对 304 响应会剔除消息体(见 framework/web/Response.php#L1102-L1103)。

因此可以这样理解成本收益:启用 HttpCache 后,未命中缓存时多付出的是计算一次时间戳/种子的代价(通常是一条轻量 SQL 或一次序列化);命中缓存时省下的是整个动作执行 + 视图渲染 + 页面传输的全部开销。这也是原文档强调「种子表达式要尽量简单」的原因。

SEO 影响

搜索引擎的爬虫趋向于遵循站点的缓存头。由于部分爬虫对同一域名在单位时间内的抓取页面数有限制,启用缓存头可以减少重复请求数量,让爬虫在有限配额内抓到更多页面,从而提升站点被索引的效率。一个合理的缓存策略(例如对不常变化的列表页设置public, max-age=3600并配合 ETag/Last-Modified 做条件请求)对用户体验与 SEO 都是加分项。

小结

  • HttpCache是 Yii 2 提供客户端缓存的标准方案,通过behaviors()声明、only/except限定动作,仅作用于GET/HEAD请求;
  • lastModifiedetagSeed是两个 callable 属性,分别驱动时间戳与内容指纹两种缓存校验,二者可同时启用并遵循 RFC 7232 的优先级规则;
  • cacheControlHeader控制通用缓存策略(默认public, max-age=3600),sessionCacheLimiter解决与 Session 自动缓存头的冲突;
  • 缓存命中时以 304 响应提前终止动作执行,服务端渲染与内容传输同时省去;
  • 相关实现与测试可深入阅读 framework/filters/HttpCache.php、tests/framework/filters/HttpCacheTest.php,以及 缓存概览 与 过滤器指南 获取完整上下文。
  • 后端
  • Web框架

【免费下载链接】yii2

Yii 2: The Fast, Secure and Professional PHP Framework

项目地址:https://gitcode.com/gh_mirrors/yi/yii2
点击查看免费下载
上一篇:5个annyang.js命令别名与同义词处理技巧:让语音识别更智能
下一篇:Scrutiny与Prometheus集成:构建完整的监控生态体系

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询