- 后端
- Web框架
【免费下载链接】yii2
Yii 2: The Fast, Secure and Professional PHP Framework
导读
HTTP 缓存(客户端缓存)是 Yii 2 四大缓存机制(数据缓存、片段缓存、页面缓存、HTTP 缓存)中唯一将缓存"下沉"到浏览器端的方案,通过条件请求(Conditional Requests)让服务器在内容未变化时直接返回304 Not Modified,从而同时省去服务端渲染与内容传输的开销。本文以 docs/guide-pt-BR/caching-http.md(与英文版 docs/guide/caching-http.md 同源)为主线,结合 HttpCache 过滤器源码 与 HttpCache 单元测试,完整讲解Last-Modified、ETag、Cache-Control三种 HTTP 缓存头部的配置方法、回源验证逻辑(If-None-Match/If-Modified-Since)、会话缓存限制器冲突处理,以及 SEO 收益,读完即可在真实控制器中落地一套可验证的客户端缓存方案。
HTTP 缓存在 Yii 2 缓存体系中的位置
在 Yii 2 的缓存体系中,服务端缓存解决的是"服务端重复计算"的问题,而 HTTP 缓存解决的是"客户端重复请求、重复传输"的问题。官方缓存总览 docs/guide-pt-BR/caching-overview.md 将缓存按层级划分为:
- 数据缓存(Data Caching):缓存数据库查询结果等基础数据,参见 caching-data.md;
- 片段缓存(Fragment Caching):缓存页面中的某一段渲染结果,参见 caching-fragment.md;
- 页面缓存(Page Caching):缓存整页渲染结果,参见 caching-page.md;
- HTTP 缓存(HTTP Caching):在浏览器端缓存最近访问过的页面内容,即本文主题。
HTTP 缓存的典型工作场景是:用户首次访问页面时,服务器正常渲染并连同校验头部(Last-Modified/ETag/Cache-Control)一起返回;浏览器将页面存入本地缓存。当用户再次访问同一页面时,浏览器携带If-Modified-Since或If-None-Match条件头向服务器发起"验证请求";若内容未变化,服务器不重新渲染页面,而是直接返回304 Not Modified,浏览器使用本地缓存版本。这样服务端渲染和页面内容传输都被跳过,两方面的开销同时得到节省。
快速上手:把 HttpCache 挂载为动作过滤器
HTTP 缓存的入口是 yii\filters\HttpCache 类。它是一个动作过滤器(Action Filter),继承自 yii\base\ActionFilter,挂载到控制器的behaviors()方法后,会在动作执行前的beforeAction阶段介入请求。
声明方式如下:
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'); }, ], ]; }需要注意的关键前提:
- 仅支持
GET与HEAD请求。从 beforeAction 实现 可以看到,若请求方法不是GET或HEAD,过滤器会直接放行(返回true),不处理任何缓存逻辑; - 至少配置
lastModified或etagSeed之一才生效。源码中当两者都为null时同样直接放行; - 过滤器通过
only/except限定作用范围(继承自 ActionFilter,支持通配符,如site/*); - 可通过
enabled属性(默认true)一键开关该过滤器。
此外,HttpCache通过beforeAction返回false来阻断动作继续执行并返回 304,其工作流程与 structure-filters.md 中描述的过滤器生命周期完全一致。
头部一:Last-Modified(最后修改时间)
Last-Modified使用一个时间戳(timestamp)来标识页面自上次被客户端缓存以来是否发生过修改。浏览器在后续请求中携带If-Modified-Since头,服务器将存储的时间与当前时间比对,若内容未变化则返回 304。
配置方式
设置 HttpCache::$lastModified 属性即可启用该头部。该属性必须是一个 PHPcallable,返回页面的最后修改时间(UNIX 时间戳)。其签名约定如下:
/** * @param Action $action 当前正在处理的动作对象 * @param array $params 属性 "params" 的值 * @return int 代表页面最后修改时间的 UNIX 时间戳 */ function ($action, $params)实战示例
以下配置表示仅对index动作启用HTTP 缓存,并以post表中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'); }, ], ]; }执行流程
- 浏览器首次访问
index页面:服务器正常渲染页面,并发送Last-Modified: <时间戳对应 GMT 时间>头部; - 浏览器再次访问同一页面且期间没有文章被修改:服务器不重新渲染页面,直接返回
304 Not Modified,浏览器使用客户端缓存版本; - 若期间有文章更新:
updated_at最大值变化,缓存失效,服务器重新渲染并返回新内容。
从源码看,服务器端将时间戳格式化为标准 HTTP 日期发送:gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'(见 beforeAction 实现)。
头部二:ETag(实体标签)
ETag(Entity Tag)用一个哈希值来代表页面的内容。页面一旦变化,哈希随之变化;通过对比客户端保存的哈希与服务器最新生成的哈希,即可判定页面是否已变更、是否需要重新传输。相比Last-Modified,ETag能表达更复杂、更精确的缓存策略——例如"站点切换了主题"这一变化并不改变Last-Modified时间戳,但会改变页面内容,此时ETag依然能正确失效。
配置方式
设置 HttpCache::$etagSeed 属性即可启用ETag头部。该属性是一个 PHPcallable,返回用于生成 ETag 哈希的"种子"(seed)字符串。其签名如下:
/** * @param Action $action 当前正在处理的动作对象 * @param array $params 属性 "params" 的值 * @return string 用作生成 ETag 哈希种子的字符串 */ function ($action, $params)实战示例
以下配置仅对view动作启用HTTP 缓存,并以所请求文章的标题与内容序列化结果作为 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]); }, ], ]; }ETag 的生成与弱校验
从 generateEtag 实现 可以看到,Yii 2 对种子做 SHA-1 哈希并 Base64 编码后包裹双引号:
$etag = '"' . rtrim(base64_encode(sha1($seed, true)), '=') . '"'; return $this->weakEtag ? 'W/' . $etag : $etag;即默认生成的 ETag 形如"AbCdEf..."。若设置weakEtag = true(自 2.0.8 起支持),则生成弱校验标签,前缀为W/(如W/"AbCdEf...")。根据 RFC 7232 语义,弱 ETag 用于"语义等价但不必逐字节相同"的内容比较,适合动态渲染页面这类字节序不稳定的场景。
另注意两点(均有 HttpCacheTest 佐证):
- 当
etagSeed返回null时,不会发送ETag头部; - 当
etagSeed返回空字符串''时,仍会正常生成并发送 ETag。
使用建议与性能警告
原文档特别强调两点:
- ETag 种子应保持简单。过于复杂的 ETag 生成逻辑会适得其反——因为 ETag 需要在每一次请求时重新计算评估,复杂计算本身会引入不必要的处理开销。应尽量找到一个简单的表达式,使其在页面内容发生变化时恰好失效即可。例如上述示例中直接用
serialize([$post->title, $post->content]),而不是去拼接整页渲染输出。 - 更精确的失效策略:
ETag可以表达Last-Modified无法表达的变化,例如主题切换导致页面外观变化但内容时间戳未变的情形。
双头部并发的 RFC 7232 行为
原文档中的一条重要技术提示(与 RFC 7232 一致):
如果
ETag与Last-Modified同时配置,HttpCache会同时发送这两个头部。而当客户端同时发送If-None-Match与If-Modified-Since时,只有前者(If-None-Match)被优先尊重。
这一行为在 validateCache 实现 中有清晰的体现:
if (Yii::$app->request->headers->has('If-None-Match')) { // HTTP_IF_NONE_MATCH takes precedence over HTTP_IF_MODIFIED_SINCE return $etag !== null && in_array($etag, Yii::$app->request->getETags(), true); } elseif (Yii::$app->request->headers->has('If-Modified-Since')) { return $lastModified !== null && @strtotime(Yii::$app->request->headers->get('If-Modified-Since')) >= $lastModified; } return false;即:请求中只要带If-None-Match,就只看 ETag 匹配结果($etag必须非空且出现在客户端发送的 ETag 列表中);否则再看If-Modified-Since(客户端时间必须不早于服务器端最后修改时间);两者都没有则缓存无效,需重新渲染。该优先级在 HttpCacheTest::testValidateCache 中有完整的参数化验证,包括对If-None-Match: *通配符的处理(匹配任意资源时不视为缓存命中)。
头部三:Cache-Control(缓存策略)
Cache-Control头部规定页面通用的缓存策略(如是否允许公共缓存、缓存有效期等)。通过配置 HttpCache::$cacheControlHeader 属性即可自定义发送的值。该属性的默认值为:
Cache-Control: public, max-age=3600即在默认情况下,HttpCache 会发送"允许公共缓存、有效期为 3600 秒(1 小时)"的策略。public表示响应可被浏览器和任何中间缓存(如 CDN、代理)缓存,max-age=3600表示缓存的最大有效时间。
需要特别说明的是源码中的细节:该属性若被设为null,则完全不发送Cache-Control头部(见 sendCacheControlHeader 实现)。因此:
- 保持默认值:发送
Cache-Control: public, max-age=3600; - 自定义字符串:按需发送,例如
Cache-Control: private, max-age=60; - 设为
null:不干预该头部。
会话缓存限制器:sessionCacheLimiter
这是 HTTP 缓存中最容易踩坑的环节。当页面使用了会话(Session)时,PHP 会根据php.ini中的session.cache_limiter配置自动发送一些与缓存相关的 HTTP 头部(典型如Expires、Cache-Control、Pragma)。这些自动头部可能与HttpCache想要的缓存策略互相干扰,甚至直接禁用掉你想获得的缓存效果。
为此,HttpCache默认(sessionCacheLimiter = '',见 属性定义)会在会话激活且头部尚未发送时,自动移除 PHP 注入的Expires、Cache-Control、Last-Modified、Pragma四个头部,从而避免干扰(见 sendCacheControlHeader 实现)。
若你想改变这一默认行为,可以配置 HttpCache::$sessionCacheLimiter 属性,其取值包括:
| 取值 | 语义 |
|---|---|
public | 允许公共缓存(允许代理/CDN 缓存) |
private | 仅允许浏览器私有缓存 |
private_no_expire | 私有缓存且不发送Expires头部 |
nocache | 禁止缓存(每次请求都回源) |
详细说明可查阅 PHP 手册中session_cache_limiter()函数(葡萄牙语版文档指引见 php.net/session-cache-limiter)。此外,若将sessionCacheLimiter设为null,则HttpCache完全不调用session_cache_limiter(),此时 PHP 将按session.cache_limiterini 配置原样发送头部。
实际使用建议:页面依赖会话(如登录态、购物车)时,通常应显式设置private或private_no_expire,避免登录用户的个性化内容被公共缓存错误地复用给其他用户;纯静态内容页则可保持默认行为并配合Cache-Control: public。
完整属性一览
结合 HttpCache 类定义,除文档中重点讲解的三个回调属性外,还有以下可配置项:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
lastModified | callable | null | 返回页面最后修改时间(UNIX 时间戳)的回调,启用Last-Modified头部 |
etagSeed | callable | null | 返回 ETag 种子字符串的回调,启用ETag头部 |
params | mixed | null | 传递给上述两个回调的附加参数(作为回调的第二个参数$params传入) |
weakEtag | bool | false | 是否生成弱 ETag(W/"..."前缀),自 2.0.8 起支持 |
cacheControlHeader | string\|null | 'public, max-age=3600' | Cache-Control头部值,null时不发送 |
sessionCacheLimiter | string\|null | '' | 会话缓存限制器值(public/private/private_no_expire/nocache),空字符串表示移除 PHP 自动发送的缓存头部,null表示不干预 |
enabled | bool | true | 过滤器总开关 |
only/except | array | [] | 动作过滤范围(继承自 ActionFilter),支持通配符 |
底层执行原理:一次请求的完整判定链
将 beforeAction 的完整逻辑展开,一次带 HTTP 缓存的请求处理链路如下:
- 前置校验:
enabled为false直接放行;请求方法非GET/HEAD直接放行;lastModified与etagSeed均为null直接放行(不产生任何缓存头部)。 - 计算校验值:分别调用
lastModified与etagSeed回调(若有配置),其中 ETag 种子会经generateEtag()加工成最终 ETag 字符串。 - 发送
Cache-Control:调用sendCacheControlHeader(),先处理会话缓存限制器(可能移除 PHP 自动头部),再设置Cache-Control响应头。 - 设置响应头:将计算出的 ETag 写入响应头。
- 验证缓存有效性:调用
validateCache($lastModified, $etag),按If-None-Match→If-Modified-Since的优先级判定客户端缓存是否仍有效。 - 条件发送
Last-Modified:仅在lastModified非空且(缓存无效,或缓存有效但未配置 ETag)时,才发送Last-Modified头部(避免缓存命中时做无谓的头部计算)。 - 命中 304:若缓存有效,设置响应状态码为
304并返回false阻断动作执行;否则返回true继续正常渲染。
这一链路与 HttpCacheTest 中的测试用例相互印证:testValidateCache覆盖了三种头部组合下的命中判定,testGenerateEtag覆盖了强/弱 ETag 的生成格式,testDisabled验证了enabled开关与无配置时直接放行的行为。
HTTP 缓存对 SEO 的影响
搜索引擎爬虫倾向于遵守缓存头部。由于部分爬虫对单个域名在特定时间窗口内可抓取的页面数量设有配额上限,引入缓存头部有助于减少需要处理的页面数量,从而帮助站点获得更充分的索引覆盖。原理在于:爬虫首次抓取后,再次请求会携带条件请求头;若内容未变化,服务器返回 304,页面不会被计入"需重新处理"的范围,爬虫的抓取配额得以节省,更多的页面因此有机会被索引。
常见问题与最佳实践
- 为什么我配置了
lastModified却没有效果?检查请求方法是否为GET/HEAD(POST等请求不会触发缓存);确认only/except是否正确圈定了目标动作;确认回调返回的是有效的 UNIX 时间戳(int)。 - ETag 回调抛异常怎么办?回调在
beforeAction中同步执行,任何未捕获异常都会中断请求;确保回调逻辑简单、可空安全(如文章被删除时findModel应妥善处理)。 - 登录用户的页面不要用
public缓存:个性化内容建议Cache-Control: private,并配合sessionCacheLimiter的private/private_no_expire,防止内容被错误共享。 ETag与Last-Modified同时配置是允许的:两者会同时下发,客户端校验时按 RFC 7232 规则优先使用If-None-Match。- 与页面缓存(PageCache)的取舍:
HttpCache只做头部级条件请求,服务器端不缓存渲染结果,适合内容变化频率低、但无法预判何时变化的页面;若内容在固定时间窗口内确定不变,可进一步叠加 caching-page.md 介绍的PageCache做服务端整页缓存,实现双重收益。 - 测试驱动验证:框架自带 HttpCacheTest 覆盖了核心判定逻辑,实际接入后可通过 curl 发送
If-None-Match/If-Modified-Since头部验证 304 行为。
小结
yii\filters\HttpCache用极小的配置成本(一个 behaviors 声明 + 一个回调)就能为 Yii 2 应用引入标准的 HTTP 客户端缓存能力:Last-Modified适合"按时间判断是否更新"的内容,ETag适合"按内容哈希判断是否更新"的内容,Cache-Control负责声明整体缓存策略,而sessionCacheLimiter保证会话页面不会被 PHP 自动头部干扰。其实现位于 framework/filters/HttpCache.php,行为由 tests/framework/filters/HttpCacheTest.php 完整锁定,开发者可以放心地将上述示例模式应用到自己的控制器中,在不改动渲染逻辑的前提下显著降低重复请求的服务端与网络开销。
- 后端
- Web框架
【免费下载链接】yii2
Yii 2: The Fast, Secure and Professional PHP Framework
相关推荐
Yii2 HTTP 缓存实战:借助 HttpCache 过滤器实现 Last-Modified、ETag 与 Cache-Control 客户端缓存
Yii2 HTTP 缓存实战:借助 HttpCache 过滤器实现 Last Modified、ETag 与 Cache Control 客户端缓存 导读 本指
后端Web框架Yii2 HTTP 缓存(HttpCache)实战指南:Last-Modified、ETag 与 Cache-Control 客户端缓存详解
Yii2 HTTP 缓存(HttpCache)实战指南:Last Modified、ETag 与 Cache Control 客户端缓存详解 本指南以 Yii
后端Web框架Yii 2 HTTP 缓存实战指南:用 HttpCache 过滤器实现客户端缓存(Last-Modified / ETag / Cache-Control)
Yii 2 HTTP 缓存实战指南:用 HttpCache 过滤器实现客户端缓存(Last Modified / ETag / Cache Control) H
后端Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考