探究Http缓存设置问题

219

为什么网站更新了没生效

为什么要刷新才能生效?到底怎么正确设置客户端/浏览器缓存,才能既让用户第一时间获取最新更新效果,又能最大化利用浏览器缓存提升用户体验,减少延迟和网络资源消耗。

一、原因

网站(静态资源、动态资源)返回的响应内容被客户端/浏览器缓存。

这个问题在移动端比较突出,因为移动端难以禁用缓存或者强制刷新页面。

二、强缓存

我简单称之为客户端/浏览器侧,为了减少资源请求,做的缓存策略,减少网络拥堵与延迟。

有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-ModifiedEtag来实现协商缓存机制,二者各有优缺点。

3条比较重要的规则:

a、由服务器确认缓存的资源是否需要更新(http 304表示不需要);

b、Last-ModifiedIf-Modified-Since分别在本次响应和下一次请求的http header中成对出现;

c、EtagIf-None-Match分别在本次响应和下一次请求的http header中成对出现;

1、探究Last-ModifiedIf-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、探究EtagIf-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.1Etag 的出现主要是为了解决几个 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-ModifiedEtag,可以尝试添加Etag响应标头,防止网络堵塞。

六、附录

1、浏览器缓存流程图

cache20220811.png