探究Http缓存设置问题
为什么网站更新了没生效
为什么要刷新才能生效?到底怎么正确设置客户端/浏览器缓存,才能既让用户第一时间获取最新更新效果,又能最大化利用浏览器缓存提升用户体验,减少延迟和网络资源消耗。
一、原因
网站(静态资源、动态资源)返回的响应内容被客户端/浏览器缓存。
这个问题在移动端比较突出,因为移动端难以禁用缓存或者强制刷新页面。
二、强缓存
我简单称之为客户端/浏览器侧,为了减少资源请求,做的缓存策略,减少网络拥堵与延迟。
有3条规则:
a、不去询问服务端,不发起任何请求,直接客户端/浏览器侧自己命中缓存;
b、除非Cache-Control
显式指定no-store
,否则无论如何,都可能存在“缓存问题(强缓存/协商缓存)”;
c、强缓存走完之后,才会走协商缓存。
1、探究Cache-Control
的机制
HTTP1.1标准下,主要通过Cache-Control
来实现强缓存机制。详细文档请见MDN,我这边不做赘述。
3条比较重要的规则:
a、Cache-Control
分为缓存请求指令和缓存响应指令
b、max-age=<seconds>
可以设置缓存存储的最大周期,超过这个时间缓存被认为过期 (单位秒)。与Expires
相反,时间是相对于请求的时间。
c、must-revalidate
可以设置一旦资源过期(比如已经超过max-age
),在成功向原始服务器验证之前,后续的请求无法使用此资源。这条比较关键,完全适用现代化前端框架的index.html
入口页.
ps: HTTP1.0标准下,主要通过Expires
来实现强缓存机制,目前咱们项目基本不用这个。
2、启发式缓存
如果一个可以缓存的请求没有设置Expires和Cache-Control,但是响应头有设置Last-Modified信息, 这种情况下浏览器会有一个默认的缓存策略:
(Date - Last-Modified)*0.1
,即1个月前更新的文档,浏览器会自动设置强缓存过期时间为30/10=3天
2条比较重要的规则:
a、我观察到,出现更新没生效的主要原因在于服务端没有提供Cache-Control
响应头;
b、一定要给资源设置合理的缓存时间,否则会默认缓存上次更新时间*0.1的时间。
三、协商缓存
浏览器强制缓存max-age
到期,开始走协商缓存(发起请求,确认缓存是否需要更新)
协商缓存主要通过Last-Modified
或Etag
来实现协商缓存机制,二者各有优缺点。
3条比较重要的规则:
a、由服务器确认缓存的资源是否需要更新(http 304表示不需要);
b、Last-Modified
和If-Modified-Since
分别在本次响应和下一次请求的http header中成对出现;
c、Etag
和If-None-Match
分别在本次响应和下一次请求的http header中成对出现;
1、探究Last-Modified
和If-Modified-Since
Last-Modified/If-Modified-Since
二者的值都是GMT格式的时间字符串, Last-Modified
标记最后文件修改时间, 下一次请求时,请求头中会带上 If-Modified-Since
值就是 Last-Modified
告诉服务器我本地缓存的文件最后修改的时间,在服务器上根据文件的最后修改时间判断资源是否有变化, 如果文件没有变更则返回 304 Not Modified
,请求不会返回资源内容,浏览器直接使用本地缓存。
当服务器返回 304 Not Modified
的响应时,response header
中不会再添加的 Last-Modified
去试图更新本地缓存的 Last-Modified
, 因为既然资源没有变化,那么 Last-Modified
也就不会改变;如果资源有变化,就正常返回返回资源内容,新的 Last-Modified
会在 response header
返回,并在下次请求之前更新本地缓存的 Last-Modified
,下次请求时,If-Modified-Since
会启用更新后的 Last-Modified
。
2、探究Etag
和If-None-Match
Etag/If-None-Match
, 值都是由服务器为每一个资源生成的唯一标识串,只要资源有变化就这个值就会改变。服务器根据文件本身算出一个哈希值并通过 ETag
字段返回给浏览器,接收到 If-None-Match
字段以后,服务器通过比较两者是否一致来判定文件内容是否被改变。
与 Last-Modified
不一样的是,当服务器返回 304 Not Modified
的响应时,由于在服务器上ETag
重新计算过,response header
中还会把这个 ETag
返回,即使这个 ETag
跟之前的没有变化。
4、优缺点
Last-Modified
因为不需要计算hash,性能好一丢丢,Etag
适用范围广些。
HTTP1.1
中 Etag
的出现主要是为了解决几个 Last-Modified
比较难解决的问题:
- 一些文件也许会周期性的更改,但是内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
- 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),
If-Modified-Since
能检查到的粒度是秒级的,使用Etag
就能够保证这种需求下客户端在1秒内能刷新 N 次 cache。 - 某些服务器不能精确的得到文件的最后修改时间。
四、缓存标头优先级
Cache-Control(强缓存) > Expires(强缓存) > Etag (协商缓存)> Last-Modified(协商缓存)
五、人大侧业务怎么处理
太长不看,直接上结论做法
1、前端推荐的做法
由于vue/react等现代化前端项目的js/css等静态资源都有版本戳,一般无需特别处理。
但是咱们人大的项目多数没有对入口页面index.html
添加Cache-Control
响应标头,导致走了浏览器默认的强缓存逻辑。
故而我们需要针对入口页面index.html
进行处理:
方式1:设置完全无缓存
Cache-Control=no-cache, no-store
方式2:设置无强缓存,有协商缓存,每次都要进行询问服务端文件是否发生变化
Cache-Control=max-age=0, private, must-revalidate
PS:nginx配置,对html页面无强制缓存
server {
listen 3335;
client_max_body_size 1024m;
client_body_timeout 600s;
location / {
root /Users/kuai/Projects/wenzhou/wenzhou-dw-app/dist;
try_files $uri $uri/ @index;
# 只对入口index.html页面:if ($request_filename ~* .*index.html)
# 针对所有html页面
if ($request_filename ~* .*\.(?:htm|html)$)
{
add_header Cache-Control "no-cache, no-store";
#add_header Cache-Control "max-age=0, private, must-revalidate";
}
}
}
2、后端推荐的做法
SpringBoot一般针对动态接口自动携带了Cache-Control=no-store
,这个无需我们操心。
推荐做法,对上传的文件加上ResourceHandler处理,这样静态资源可以加上Cache-Control
强缓存和协商缓存Last-Modified
,不用每次都去接口请求,造成大量资源浪费。
而巨化的项目上传和更新文件基本用的都是唯一的uuid去创建文件夹名称,形成独一无二的url路径,完全可以将强缓存Cache-Control
设置一个比较大的值(一个月)来减少服务器资源消耗。
ps: springboot添加静态资源路径
// WebMvcAutoConfiguration.java
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//this.appProperties.getUploadDir: /data/web-files/zjrd-dms/files
//访问路径 http://localhost/files/aaa.txt
registry.addResourceHandler("/files/**").addResourceLocations("file://" + this.appProperties.getUploadDir());
}
如果是文件流下载接口,没有Last-Modified
或Etag
,可以尝试添加Etag
响应标头,防止网络堵塞。