概述
HTTP缓存的特点充分体现在B/S
或C/S
体系架构中,例如:跟用户接触的各种客户端(浏览器/应用程序),还有代理服务器(正向/反向/透明)和CDN等中间缓存/代理缓存。也就是说,HTTP缓存的范围在[客户端 , 源服务器)
。通过制定HTTP Header缓存策略
来实现客户端缓存或代理缓存,以提高用户体验和减少带宽压力。
缓存策略
目前,在可缓存范围内默认自动
缓存一些不常改变
的资源。一般的判断途径有如下两点:
- 请求方法:GET、HEAD
- 返回的状态码:206返回的不完整的资源、301永久重定向的URL和404返回的空页面等
若使用其它不可缓存的请求方法去请求已缓存的URI,则此URI下的缓存会失效
客户端缓存
默认
在cache-control中加入了private
,表示只有客户端才能缓存,以下以浏览器为代表。缓存中存储着请求过的可缓存资源(包括响应头信息),其实也就是缓存着HTTP响应报文
。
#HTTP/1.1 优先级高
## 强制缓存 (单位秒)
cache-control: max-age=600
##校验缓存 (与private一样默认存在响应头中,虽然没有显示出来)
cache-control: no-cache
#----------------------------------------------------#
#HTTP/1.0 优先级低,兼容作用
##强制缓存
expires: Mon, 25 Sep 2020 23:39:34 GMT
##校验缓存
pragma: no-cache
#----------------------------------------------------#
#其它
##不缓存资源
cache-control: no-store
##缓存资源必须校验后才能使用(不论是否过期)
cache-control: must-revalidate
#----------------------------------------------------#
#实验阶段的首部
##不变的资源,即使刷新也不校验缓存
cache-control: immutable
##下面两个一般会结合着用
##在x秒内可以先暂时使用过期资源,异步进行校验更新
cache-control: stale-while-revalidate=x(秒)
##异步校验更新失败时,如果在x秒内的话,那么还将使用过期资源
cache-control: stale-if-error=x(秒)
强制缓存
- 强缓存/强制缓存:在
max-age
或expires
时间内,浏览器不会重新发送请求到服务器。响应头date
值通常需要转换为系统当前时区,这里应转为GMT+8北京时间。
强制缓存剩余时间 = ( 响应时间date + 允许缓存时间max-age ) - 请求时间(当前系统时间)
总结
响应头 | 优先级 | 来源 | 精确程度 |
---|---|---|---|
cache-control: max-age=600 | 高 | 由客户端去计算 | 较准确(除非用户修改系统时间 😑) |
expires: Mon, 25 Sep 2020 23:39:34 GMT | 低 | 由服务器生成 | 存在时区和客户端等因素,误差无法控制 |
响应头 | 作用 | 其它 |
---|---|---|
cache-control: no-store | 不缓存资源 | 效果相当于cache-control: no-cache=Location(只能在响应头) |
cache-control: no-cache | 不缓存过期资源 | 在请求头 中的作用是直接向源服务器请求资源 |
疑问:当no-store和no-cache同时存在时,缓存策略会如何选择
响应头 | 作用 | 其它 |
---|---|---|
cache-control: must-revalidate | 缓存资源必须 到源服务器校验后才能使用 | 跟proxy-revalidate的效果一样,只是这个作用范围是全局 的(适用于客户端和缓存服务器) |
校验缓存
- 对比缓存/协商缓存/校验缓存:当强制缓存没有设置或已过期,就会检测缓存资源是否存在
etag
或last-modified
(按优先级),然后携带对应的if-none-match
或if-modified-since
请求头到服务器中校验一致性。若一致,则返回304并更新缓存信息(date等),然后从缓存中获取相应资源;若不一致,则返回200并在body中携带该资源,同样也会更新缓存信息。
etag值在Nginx中默认通过计算
last-modified
与content-length
生成形如:etag: W/"5f6967ee-357e"。W表示Weak,弱etag
的意思。
touch
命令可以很容易地修改文件的最后修改时间,内容长度也可以在修改后保持不变。如果想要避免误差,尽量自定义生成强etag
,可以采用GUID、MD5、sha1或者更强的散列值,如:sha256或sha512等。
总结
请求头/响应头 | 优先级 | 来源 | 精确程度 |
---|---|---|---|
if-none-match/etag | 高 | 默认生成16进制的组合值,消耗性能 | 较准确(因算法而异) |
if-modified-since/last-modified | 低 | 直接获取文件的last-modified值,比较容易 | 误差在1秒内 |
其它
除了前面提到的首部外,还有一些针对客户端的不常用
的请求头,一些可能存在兼容性问题,了解即可。
请求头 | 作用 |
---|---|
cache-control: max-stale=x(秒) | 客户端可接受过期不超过x秒的资源 |
cache-control: min-fresh=x(秒) | 要求缓存服务器中响应资源的age 不超过x |
cache-control: no-transform | 跟下面的代理缓存一节中说的一样 |
cache-control: only-if-cached | 客户端只接受已经缓存的资源响应 |
代理缓存
必须显式
在cache-control中加入public
,才能使中间缓存/代理缓存生效。实际上,客户端并不知道自己发送的请求是由缓存服务器
响应的,还是由源服务器
响应的。
源服务器针对缓存服务器的相关首部设置
#缓存服务器常见的响应指令,多个值用逗号隔开
cache-control: public, s-maxage=100, no-transform, proxy-revalidate
##缓存计时 (单位秒)
age: 9593360
##值的类型为<header-name>
vary: Origin, Accept-Encoding, User-Agent, Cookie
总结
响应头 | 作用 |
---|---|
cache-control: public | 允许使用代理缓存 |
cache-control: s-maxage=100 | 覆盖max-age和expires,只适用于缓存服务器 |
cache-control: proxy-revalidate | 必须向源服务器重新验证资源有效性才能返回给客户端,只适用于缓存服务器 |
cache-control: no-transform | 例如:又拍云图床CDN会将图片转成webp缓存以提升响应速度,此指令是不允许的 |
age: 9593360 | 资源在缓存服务器上停留的时间(缓存服务器系统当前时间 - 源服务器响应时间) |
vary: User-Agent | 根据header-name来缓存资源,这里表示根据客户端类型缓存资源(PC端请求返回PC端缓存;Browser端请求返回Browser端缓存) |
当缓存服务器违反了资源响应头中的声明或出现其它意外状况时,就会给客户端返回warning警告码
存疑
public与private
在前面所提到的private与public的区别是网上普遍认同的,但在《图解HTTP》一书中谈到的private却有所不同,它表明private也可以缓存在缓存服务器上。只是这个缓存不是跟public一样可以无差别响应给所有到来的请求,而是响应给特定用户
。这应该是根据Cookie来标识的吧,有条件的可以测试一下。
缓存位置
优先级从上至下依次降低
- Service Worker:运行在浏览器背后的独立线程,仅支持HTTPS加密。可以由开发者自由控制缓存哪些文件、如何读取和匹配缓存等,且缓存可持续有效。到flokk页面中体验一下吧。
- memory cache:关闭标签页后失效(临时存储位置,响应迅速,忽略强制缓存和校验缓存的束缚)
- disk cache:保存在硬盘长久有效,除非用户主动清理(强制缓存和校验缓存的主要存储位置)
- Push Cache:HTTP2中的推送缓存,只在Session会话期间有效,时效极短。虽然可以推送任何资源,但只能使用一次。功能上的支持依赖于浏览器,详情参考HTTP/2 push
缓存的目的是提高用户访问速度和减少带宽占用,所以真正的物理缓存位置目前只有memory cache和disk cache两种,而Service Worker和Push Cache只是一种控制缓存的技术,它们不受HTTP Header缓存策略的影响。Service Worker通过请求拦截技术,根据自定义的逻辑和优先级从memory cache或disk cache中取出资源,但请求中显示的还是from Service Worker
。Push Cache是HTTP2的技术,浏览器有针对的缓存策略。
一个有趣的地方是,CSS资源总是优先存储在disk cache中,因为它只需要在页面渲染时加载一次;而JS、字体和图片等需要频繁解析或加载,所以优先存储在memory cache
用户在浏览器上的行为
常见操作
- 键入网址后Enter(直接访问):按上面的优先级获取缓存数据(根据
memory cache
的时效性,一般是没有从那里返回的数据,除非用户将未关闭的标签页网址放到新建标签页中再次访问),若没有则向服务器发送请求。 - Ctrl + R/F5(普通刷新):按上面的优先级获取缓存数据,若没有则向服务器发送请求
- Ctrl + Shift + R/Ctrl + F5(强制刷新):直接向服务器发送请求
三种行为的异同
不同点
- 直接访问会先后触发强制缓存和校验缓存
- 普通刷新会触发校验缓存,请求会带上
cache-control: max-age=0
(还有if-none-match和if-modified-since) - 强制刷新会忽略缓存,请求会带上
cache-control: no-cache
(不会带上if-none-match和if-modified-since),为了兼容还会带上pragma: no-cache
。如果在控制台上勾选Disable cache
,那么也相当于强制刷新的效果。
相同点
这三种操作只要没有触发强制缓存,那么都会更新所请求资源的缓存信息,也就是相关的头信息,如:date/expires/etag/last-modified等。不过,通过直接访问的方式很难不触发强制缓存,除非刚好过期或者开发者没设置 😑
其它
- 前进和后退:同样是按上面的优先级获取缓存数据,唯一不同的是,按下前进或后退按钮还会从
标签页会话历史
中恢复该页面最后记录的状态(该状态包括当时的响应头信息和滚动条位置等),称为标签页会话状态
。新版浏览器还加入了Back-forward cache
的功能,能从内存
中恢复当时的完整状态(包括JavaScript和DOM)。该功能在Chrome中还处于实验阶段,默认未启用,详情打开chrome://flags/
搜索。 - Ctrl + Shift + T:恢复上一次关闭的标签页和会话历史,其余效果跟直接访问类似。
- Ctrl + Shift + delete:清空缓存
Nginx配置
在新版Nginx中,当expires
被设置后,会根据当前系统时间计算并生成Cache-Control相应的max-age信息,以排除因时区差异所造成的影响。还有就是,Nginx默认会生成etag
和last-modified
响应头信息,也就是默认存在校验缓存,我只需要决定是否启用强制缓存即可 😋
server {
#优先级从里到外,若location里面设置了add_header,则会忽略外面的设置
location / {
index index.html index.htm;
expires 7d;
}
location ~\.html$ {
try_files $uri =404;
expires 7d;
}
location ~*\.(gif|svg|jpg|jpeg|png|bmp|ico|svg)$ {
expires 365d;
}
location ~*\.(js|css) {
expires 7d;
}
location ~*\.(mp3|ttf|woff|woff2) {
expires 365d;
}
}
上述设置因人而异,图片/字体等静态资源不会频繁更新,我将其设置成强制缓存1年;html/js/css等资源会被我频繁改动,所以强制缓存7天就好了。
资源如何更新
问题
当我们访问一个网站时,需要向服务器发送大量资源请求,而对于这些请求,浏览器是如何得知的呢?那是因为浏览器会先将要访问的网页资源加载进来,根据网页中嵌入的链接地址再去分别请求,如:JS、CSS、字体和图片等,此HTML网页相当于访问入口。
问:网页校验缓存返回200之后,其中的链接会如何获取资源?
如果链接地址没有改变,则根据HTTP Header缓存策略执行即可;若已改变,则重新发起请求呗~。但存在一种情况,例如:https://www.livejq.top/js/myfile.js
资源链接。虽然我修改了这个资源,但文件名/链接没有变化,那么浏览器当然是不会主动重新去请求的,这种情况是非常糟糕的,会造成排版混乱甚至是内容不一致
。在浏览器上,“不一般的用户”还可以执行手动强制刷新或清空缓存等操作,而在一些手机客户端上往往操作有限。
解决办法
给文件名添加版本号:https://weatherwidget.io/w/js/angular-1.5.8.min.js
给文件名添加编号(散列值或更新日期等):https://ssl.gstatic.com/accounts/o/1209952130-idpiframe.js
给文件添加版本路径:https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min.js
注意:现已不推荐使用类似
myfile.js?v=1.2
的QueryString方式,这会阻止某些代理服务器去缓存它
以上只是个人理解,难免错漏,仅供参考~
评论区