Expires, Last-Modified, Etag缓存机制

6,234

前言

最近遇到了一个问题,我们团队开发了一个js库,提供给各个业务方引入,采集相关业务数据,渐渐发现,每次更新js库的代码,重新发布,并清除dns缓存后,还是有些业务方在请求旧的js库的代码,怎么才能让业务方拿到最新的sdk代码呢?我陷入了困扰,每次重新发布修改js库的文件名?这似乎不太合理,接入的业务方那么多,每次发布一个个去通知他们更改引入的文件名?怕是会被业务方们扔鸡蛋,且更改了文件名业务方也不会因为这个小改动重新发版,这显然不靠谱.于是我研究了一下浏览器的静态资源缓存的策略,原以为nginx配置那里修改相应头的Etag字段,告诉浏览器我这个资源更新了,浏览器便会从服务器下载新的资源文件了.但后来经过一番折腾后,发现nginx只能保证ETag开启和关闭,也就是详情头里是否包含该字段,但是并不能保证资源更新后该ETag字段的更新,我的问题并没有解决,但是花功夫仔细研究了一下Expires, Last-Modified, Etag的缓存机制.在此,我把我对浏览器的静态资源缓存策略的相关内容做一个分享,希望可以帮助遇到类似困扰的童鞋们~

Cache-Control(缓存控制)

在开始重点内容的分享之前,我想先分享一下关于请求头和响应头中的Cache-Control字段,这个字段在http请求中充当着缓存控制的角色,是控制缓存的开关,用于标识请求或访问中是否开启了缓存,使用了哪种缓存方式.

Cache-Control通用消息头字段被用于在http请求和响应中通过指令来实现缓存机制,缓存指令是单向的,这意味着在请求设置的指令,在响应中不一定包含相同的指令.Cache-Control响应指令允许源服务器覆盖一个响应默认的缓存功能.

Cache-Control字段

在请求中使用Cache-Control,它可选的值有:

字段名称 说明
no-cache 告知(代理)服务器不直接使用缓存,要求向原服务器发起请求(这并不代表你每次都可以取到最新的资源)
no-store 所有内容都不会被保存到缓存或Internet临时文件中
max-age=delta-seconds 告知服务器希望接收一个存在时间(Age)不大于delta-seconds秒的资源
min-fresh=delta-seconds 告知(代理)服务器客户端希望接一个在小于delta-seconds秒内被更新过的资源
no-transform 告知(代理)服务器客户端希望获取实体数据没有被转换(比如压缩)过的资源
only-if-cached 告知(代理)服务器客户端希望获取缓存的内容,而不用向原服务器发去请求
cache-extension 自定义扩展值,若服务器不识别该值将被忽略掉

在响应中使用Cache-Control时,它可选的值有:

字段名称 说明
public 表明任何情况下都得缓存该资源(即使是需要HTTP认证的资源)
Private[="field-name"] 表明返回报文中全部或部分(若指定了field-name则为field-name的字段数据)仅开放给某些用户(服务器指定的share-user,如代理服务器)做缓存使用,其他用户则不能缓存这些数据
no-cache 不直接使用缓存,要求向服务器发起(新鲜度校验)请求(这就到了Etag和Last-Modified起作用的时候了)
no-store 所有内容都不会被保存到缓存或Internet临时文件中
no-transform 告知客户端缓存文件时不得对实体数据做出任何改变
only-if-cached 告知(代理)服务器客户端希望获取缓存的内容(若有),而不用向原服务器发去请求
must-revalidate 当前资源一定是向原服务器发去验证请求的,若请求失败会返回504(而非代理服务器上的缓存)
proxy-revalidate 与must-revalidate类似,但技能应用于共享缓存(如代理)
max-age=delta-seconds 告诉客户端,该资源在delta-seconds秒内是新鲜的,无需向服务器发请求
s-maxage=delta-seconds 同max-age,但仅应用于共享缓存(如代理)
cache-extension 自定义扩展值,若客户端不识别该值将被忽略掉

需要注意的是,在Cache-Control中,这些值可以自由组合,多个值冲突时,是有优先级的,no-store优先级最高.

缓存校验: 在缓存中,我们需要一个机制来验证缓存是否有效.比如服务器的资源更新了,客户端需要及时刷新缓存,又或者客户端的资源过了有效期,但是服务器上的资源还是旧的,此时并不需要重新请求资源.缓存校验就是用来解决这些问题的.

Expires(缓存校验)

服务器设置Expires字段为一个日期,客户端请求该资源时将这个日期与客户端当前日期进行比对,如果当前时间小于这个日期,则表示资源未过期,使用缓存,如果当前时间大于这个日期,则表示资源已过期,客户端就会重新请求该资源.但是这一策略会收到客户端与服务器时间不一致的问题的影响.如果客户端时间晚于服务器的时间,会导致资源还未过期就重新请求,反之,会导致客户端还在使用过期的旧资源.

Last-Modified / If-Modified-Since(缓存校验)

当浏览器第一次请求一个资源时,服务端返回状态码200,返回请求的资源的同时HTTP响应头会有一个Last-Modified标记着文件在服务端最后被修改的时间.

浏览器第二次请求上次请求过的资源时,浏览器会在HTTP请求头中添加一个If-Modified-Since的标记,用来询问服务器该时间之后文件是否被修改过

如果服务器端的资源没有变化,则自动返回304状态,使用浏览器缓存,从而保证了浏览器不会重复从服务器端获取资源,也保证了服务器有变化时,客户端能够及时得到最新的资源.

Etag / If-None-Match(缓存校验)

当浏览器第一次请求一个资源时,服务端返回的状态码为200,同时HTTP相应头会有一个Etag字段,存放着服务器端生成的一个序列值.

浏览器第二次请求上次请求过的url时,浏览器会在HTTP请求头添加一个If-None-Match的标记,用来询问服务器该文件有没有被修改。

如果服务器的资源没有变化,Etag字段没有被修改依然与If-None-Match的值保持一致,则请求自动返回304状态,使用浏览器缓存.如果不一致,则说明资源被更改,则重新去下载新的资源.

Etag主要为了解决Last-Modified无法解决的一些问题:

(1) 一些文件也许周期性的更改,但是它的内容并不改变(仅仅改变的是修改时间),这个时候我们不希望客户端认为这个文件被修改了,而重新获取资源.

(2) 某些文件修改非常频繁,比如在秒一下的时间内进行修改(比如1s内修改了N次),If-Modified-Since能检查到的粒度是秒级的,这种修改是无法判断的(或者说UNIX记录MTIME只能精确到秒);

(3) 某些服务器不能精确的得到文件的最后修改时间;

nginx配置里ETag选项默认开启的,所以请求的资源文件若发生改动,会在响应头里生成新的ETag值.这样客户端就能够发现If-None-Match的值和Etag字段的值不匹配,从而去请求最新的资源文件.

我的困惑

静态资源修改以后.对应的文件最后更改时间和ETag字段有时似乎都不会做相应的更改,浏览器缓存该静态资源文件,导致在文件更改后不能及时更新.是不是静态资源的缓存只能依靠修改文件名的策略来拿到最新的资源? 那么Expires, Last-Modified, Etag这三个字段只能在后台代码里才能修改吗?各路英雄可还有更好的解决方案?望各路英雄不吝赐教~

往期文章

在vue中使用SockJS实现webSocket通信

postMessage可太有用了

手把手教你制作表格表头悬浮(table-header-fixed)