基石篇------HTTP缓存策略与构建哈希原理
本文核心:为什么"HTML永远最新,静态资源永不更新"是前端部署的第一性原理?我们将从HTTP协议规范、浏览器缓存驱逐算法、Webpack/Vite哈希生成机制三个维度彻底讲透。
一、HTTP缓存规范:强缓存与协商缓存的"三角博弈"
很多工程师对缓存的认知停留在"设置个max-age就完了",实则不然。浏览器缓存决策的完整链路是:内存缓存(Memory Cache) → 磁盘缓存(Disk Cache) → 服务端协商(Validation)。
1. 强缓存(Strong Caching)
通过Cache-Control和Expires(HTTP/1.0遗留,现在基本不用)控制。关键点在于Cache-Control的指令组合:
public:表示响应可以被任何中间代理(CDN、反向代理)缓存。private:仅允许浏览器缓存,不允许代理缓存(通常用于含用户隐私数据的接口,但前端静态资源极少用)。immutable:这是Chrome/Firefox近年支持的指令,表示资源"永远不会变",即使刷新页面或关闭重开,浏览器也完全不会 发起条件请求(连If-Modified-Since都不发),直接走本地磁盘。这对带哈希的JS/CSS是神配置。stale-while-revalidate:这是CDN层面的杀手锏,后面会细讲。
配置误区 :很多人写Cache-Control: max-age=31536000就完了。正确姿势应该是:
nginx
location ~* \.(js|css|png|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable, max-age=31536000";
}
注意 :expires指令计算的是服务端时间,如果服务端时间与客户端时间有偏差(常见于云服务器时区未校准),会导致缓存过期计算偏差。因此,推荐仅使用Cache-Control,摒弃expires。
2. 协商缓存(Validation)
当强缓存过期或禁用时,浏览器携带If-None-Match(ETag)或If-Modified-Since(Last-Modified)去服务器验证资源是否修改。
- ETag vs Last-Modified :ETag优先级高于Last-Modified。ETag由服务端根据文件内容(inode+size+mtime,或直接内容hash)生成。坑点 :如果你的静态资源走Nginx,Nginx默认生成的ETag是基于
inode的。如果文件内容没变但inode变了(比如重新上传覆盖),ETag就变了,导致协商缓存失效,引发不必要的流量。解决方案:Nginx配置etag on;(默认开启)且if_modified_since before;,但对JS/CSS我们一年强缓存根本不走协商,因此无需纠结。
对于index.html,必须禁用强缓存并正确配置协商缓存:
nginx
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache"; # 兼容HTTP/1.0
etag on; # 开启ETag,让浏览器每次都会验证
}
这里no-cache和no-store的区别必须分清:no-cache意为"可以缓存,但每次使用前必须向服务器验证"(走协商);no-store意为"彻底不缓存,每次都去服务器拿完整数据"。对于HTML,两者结合是最保险的。
二、构建哈希(Hashing)的前世今生:Webpack 4 -> 5 的进化
前端构建工具的核心目标之一就是"给文件打上独一无二的指纹"。这背后经历了漫长的迭代。
1. [hash]、[chunkhash]、[contenthash]的区别
[hash](Webpack 4起就不推荐) :每次构建全局所有文件共用一个hash。只要你改了任何一行代码,所有文件的hash都会变,缓存全部失效------致命缺陷。[chunkhash]:基于Webpack的Chunk分组生成hash。一个Chunk内部包含JS和引用的CSS(通过MiniCssExtractPlugin)。但坑在于:如果JS和CSS在同一个Chunk,改JS会导致CSS的hash也变,浪费了CSS缓存。[contenthash](Webpack 5默认推荐):严格基于文件二进制内容生成MD5或SHA-256摘要。只要文件内容不变,hash雷打不动。
Webpack 5 的突破 :引入了确定性模块ID(optimization.moduleIds: 'deterministic'),配合[contenthash],解决了热更新时模块ID变化导致vendor chunk hash变化的老大难问题。
2. Vite(Rollup)的哈希策略
Vite底层基于Rollup,对[contenthash]的支持非常彻底。在vite.config.ts中配置:
typescript
export default defineConfig({
build: {
rollupOptions: {
output: {
entryFileNames: 'js/[name].[contenthash].js',
chunkFileNames: 'js/[name].[contenthash].js',
assetFileNames: 'assets/[name].[contenthash].[ext]'
}
}
}
});
特别注意 :Vite在开发模式下(serve)不生成hash,仅在build时生成,且依赖package-lock.json来稳定依赖结构,提升构建的确定性。
三、浏览器缓存淘汰算法(Cache Eviction):硬盘满了怎么办?
很多人不知道,浏览器对磁盘缓存是有大小限制的(Chrome通常限制在几百MB到1GB)。当缓存满时,浏览器会基于 LRU(Least Recently Used,最近最少使用) 或 LIRS(Low Inter-reference Recency Set,低交叉引用最近使用集) 算法淘汰旧资源。
这对我们有什么启示? 如果你的vendor.js是2MB,用户在访问了20个其他网站后,你的vendor.js可能已被浏览器从磁盘删除了(即使max-age还没到)。这时浏览器会重新请求,并带上If-None-Match(因为有ETag)去验证。如果我们的index.html是no-cache,且静态资源服务器能正确响应304 Not Modified,那么流量消耗极小,但重新下载(如果没命中缓存)仍需时间。
终极解法 :使用 Service Worker 接管缓存(即PWA策略),SW可以自己管理CacheStorage API,拥有独立的存储配额(通常比HTTP Cache更大且更稳定),不受LRU驱逐影响。这是第二层容灾的基石,我们将在第3篇展开。
四、实战:Nginx + CDN 的响应头协商陷阱
典型案例 :某电商项目将index.html也上传了OSS并开启了CDN加速,且CDN设置了Cache-Control: max-age=600(10分钟)。结果每次发布后,用户10分钟内看到的都是旧版HTML,导致页面炸裂。
正确做法:
- HTML绝不走CDN长缓存 。如果必须走CDN,在CDN控制台设置
缓存时间为0秒(或follow origin,源站返回no-cache)。 - 区分"回源"与"边缘"缓存 。CDN的
Cache-Control通常有两层:源站响应头(优先) > CDN控制台配置。建议强制在源站Nginx设置响应头,并让CDN"遵循源站",避免控制台配置遗漏或误操作。
Nginx完整配置模版:
nginx
server {
listen 80;
root /var/www/html;
index index.html;
# 针对带hash的静态资源(正则匹配)
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$ {
# 如果文件名包含hash(如 .[a-f0-9]{8,} ),则直接强缓存一年
if ($request_filename ~* "\.[a-f0-9]{8,}\.(js|css)$") {
add_header Cache-Control "public, immutable, max-age=31536000";
}
# 若不包含hash(少数情况),则降级为协商缓存
add_header Cache-Control "no-cache";
try_files $uri $uri/ =404;
}
# HTML文件
location ~* \.html$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
# 开启协商,但ETag由Nginx计算,基于文件修改时间+大小
etag on;
if_modified_since exact;
}
}
注意 :if在location上下文中是"邪恶的"(官方文档警告),但对于简单的正则匹配文件名,性能损耗可忽略。更优雅的方式是在构建时直接区分目录(如/static/hash/和/),然后用location /static/直接处理。
五、本章总结与下篇预告
第一层漏斗的根基在于缓存策略与文件指纹。理解浏览器缓存的全链路决策(强缓存→协商缓存→缓存淘汰),以及Webpack/Vite的哈希进化史,是部署工程师的基本素养。
但发版不只是"扔文件",如何在不中断服务的情况下让用户平滑过渡到新版本?这就引出了蓝绿部署与金丝雀发布 。下一期我们将深入四层/七层负载均衡的底层原理,揭开Nginx upstream权重切换与K8s Ingress流量染色的神秘面纱,并重点讨论------数据库迁移如何与蓝绿部署协同(这是90%的团队都会踩的坑)。