在你访问互联网中的任何资源其所产生的任何链路中的每一个节点几乎都会进行缓存,整个缓存体系和细节十分复杂。比如浏览器缓存,服务器缓存,代理服务器缓存,CDN缓存等。
但是缓存又十分重要,不可缺少,为什么这么说呢?
由于HTTP请求的链路漫长,环境复杂,把一些必要的信息在关键的节点进行缓存,下次请求的时候可以尽可能的复用数据,就可以节省一部分资源的消耗,减少HTTP请求应答的成本,节约带宽,加快响应速度。
那么,基于请求-应答模式的特点,缓存大致可以分为服务器缓存和客户端缓存,而服务器缓存经常与代理服务关联在一起。这次我们主要关注浏览器是如何进行缓存的。
假设,现在没有缓存,我们想象一下获取资源的方式是什么样的?
客户端请求资源,服务器返回资源,等下一次想要获取同样资源的时候,哪怕服务器的资源并没有更新,还是要重新走一遍网络请求,然后服务器返回资源的完整链路。
那如果有了缓存,在客户端第一次获取到请求的资源后,把资源缓存到本地,下次再去请求的时候,发现本地有这个资源,直接拿来用就好了,完全不用去走网络请求。
HTTP缓存都是从第二次请求开始的,可以概述如下:
第一次请求资源时:
服务器返回资源,并在 response header(响应头) 中回传资源的缓存策略;
第二次请求时:
浏览器判断这些请求参数,击中强缓存就直接200;
否则就把请求参数加到request header(请求头)中传给服务器,看是否击中协商缓存,击中则返回304,否则服务器会返回新的资源。
我们仔细的阅读一下这个简单的缓存资源请求流程,发现其中有几个重要的节点。
首先,服务器在返回该资源时,要标记该资源的有效期。然后,浏览器初次请求肯定是没缓存的,再次请求的时候,它要根据该资源的有效期来判断下一步该怎么办。
我们再简单一点,如果我们试图去获取缓存资源,其实是要看服务器的标记的。那么换句话说,服务器标记缓存资源,浏览器会验证该缓存资源的标记。
浏览器每次发起请求时,会先在浏览器缓存中查找该请求的结果以及缓存标识,根据缓存标识来判断是否使用本地缓存。如果缓存有效,则使用本地缓存;否则,则向服务器发起请求并携带缓存标识。
根据是否需向服务器发起请求,将缓存过程划分为两个部分:强制缓存和协商缓存,强缓存优先于协商缓存。
浏览器缓存控制机制有两种:HTML Meta 标签 和 HTTP 头信息
HTML Meta标签是应用在HTML文件中的head头部分。
<meta http-equiv="Cache-Control" content="max-age=7200" />
// or
<meta http-equiv="Cache-Control" content="no-cache" />
主要作用就是告诉浏览器此HTML页面不被缓存,每次访问都去服务器上下载。
使用上很简单,但这个是 IE 时代的私有属性,在 IE9 以前支持的,而现在主流的 Chrome / Firefox / Safari,包括 IE9 ~ IE11 都不支持。而且所有缓存代理服务器都不支持。因为代理不解析HTML内容本身。
所以我们常说的浏览器缓存还是通过HTTP头信息来控制缓存。
http-equiv
倒确实在 HTML 规范中有几个值可以设(content-security-policy
、content-typ
e、default-style、x-ua-compatible
、refresh
),但都跟缓存无关,可以参阅文档 http-equiv。
使用 HTTP 头信息进行缓存处理一般是通过设置相应的请求头/响应头实现的
常见的处理方式有 4 种
Cache-Control
Expires
Etag
/ If-None-Match
Last-Modified
/ If-Modified-Since
后3种是 http 1.0 就已经支持的,Cache-Control
是 http 1.1 支持的
缓存优先级: Cache-Control
> Expires
> Etag
> Last-Modified
Cache-Control
和 Expires
首部用于指定缓存时间,Last-Modified
和 ETag
首部提供验证机制。
通过浏览器开发者工具我们可以看到,浏览器请求服务器静态资源的响应状态码主要就是下图的四种:
通过 Expires
(http1.0) 和 Cache-Control
(http1.1) 两个响应头字段来控制,如果同时存在,则后者优先级高于前者。
服务器通知浏览器一个缓存时间,在缓存时间内,下次请求直接从本地缓存中读取资源而不发起请求。不在时间内,执行比较缓存策略。
强缓存命中则直接读取浏览器本地的资源,在network中显示的是from memory cache
或者from disk cache
。
Cache-Control
是一个相对时间,用以表达自上次请求正确的资源之后的多少秒的时间段内缓存有效。
Cache-Control
的出现是为了解决 Expires
在浏览器时间被手动更改导致缓存判断错误的问题。
Expires
是http响应头字段,是一个绝对时间,用以告诉浏览器这个时间点之前发起请求可以直接从浏览器中读取数据,而无需发起请求。
Expires
字段为一个日期, 客户端请求该资源时将这个日期与客户端当前日期进行比对。优势:
劣势:
Cache-Control
通用消息头字段,被用于在 http 请求和响应中,通过指定指令来实现缓存机制。
缓存指令是单向的,这意味着在请求中设置的指令,不一定被包含在响应中。
一般用在http响应中进行客户端缓存设置。
Cache-Control
是在 HTTP/1.1 中才有, 算是一个"新"的API (新是相对以前的处理, 因为现在已经有 HTTP3 了)
可选的参数有很多: max-age=
,no-store
,no-cache
,must-revalidate
,private
,public
等
在HTTP/1.1中增加的字段。属于相对时间。该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务器发送请求。比Expires
多了很多选项设置。
max-age
可以用来表示过期时长,单位是秒。表达式是这样子的:
Cache-Control: max-age=30
该资源的有效期是30秒。
这是一个相对时间,时间计算的起点是报文创建的时刻,也就是响应头Date字段的时间,是指资源离开服务器的时刻,而不是客户端收到报文的时候。
换句话说,假设我们设置的时间是5秒,但是链路请求很长,花费了4秒的时间,那么缓存对于浏览器的有效时间只是1秒。
那你可能会说,就这一秒有啥用啊~~假设你网站的访问量特别大,每秒有上百万的访问,那你可以想象到这仅仅一秒的时间能节省服务器多大的压力了吧。当然,只是举个极限的例子~
我们先搞个max-age
的小例子,看看缓存能否生效:
res.setHeader("Cache-Control", "max-age=20");
我们可以看到在响应头中返回了我们设置好的缓存字段:
除了max-age这个最常用的属性以外,还有三个属性可以更精确的指示浏览器如何使用缓存
no-store
浏览器和代理服务器禁止缓存,每次只能去服务器请求最新资源。用于某些变化非常频繁的页面,比如秒杀页面。
Cache-Control: no-store
no-cache
它的字面意思和no-store
很容易搞混,实际上它的意思并不是不允许缓存,而是可以缓存,只是不使用强缓存,每次使用前必须要去服务器验证是否过期,是否有最新的版本。同时,代理服务器也不能对资源进行缓存。一般用于长期不变的资源,客户端只需要发送很小的信息跟服务端进行确认 (协商缓存验证)。
must-revalidate
和no-cache
又很相似,它的意思是缓存不过期就可以继续使用,但是过期了如果还想用就必须去服务器验证一下。
private
只有浏览器可以缓存
public
浏览器,服务器,代理服务器都可以缓存
Cache-Control
时指定 no-cache
或 max-age=0, must-revalidate
表示客户端可以缓存资源,但每次使用缓存资源前都必须重新验证其有效性。这意味着每次都会发起 HTTP 请求,但当缓存内容仍有效时可以跳过 HTTP 响应体的下载。
Cache-Control: no-cache
# or
Cache-Control: max-age=0, must-revalidate
优势:
Expires
服务器和客户端相对时间的问题。Expires
多了很多选项设置。劣势:
协商缓存有 2 组字段(不是两个),控制协商缓存的字段有:Last-Modified
/ If-Modified-Since
(http1.0)和 Etag
/ If-None-Match
(http1.1)。如果同时存在,则后者优先级高于前者。
Last-Modified
很好理解,就是服务端返回最后一次修改文件的时间。
If-Modified-Since
(客服端发起): 本地文件的修改时间(可以理解为上次请求返回的 Last-Modified
的值),以后每次请求,请求头都会都带上。
如果强制缓存未命中,但协商缓存可用,需要第一次请求的时候,HTTP响应头中返回Last-Modified
或ETag
,当第二次请求的时候请求头会自动设置 If-Modified-Since
或者 If-None-Match
的值,服务端根据值,去决策验证是否命中协商缓存。
如果命中了协商缓存,那么服务器仅返回**304(不返回数据实体)**状态码,更新下资源的有效时间,使用本地缓存,因此在响应体体积上的节省是它的优化点。没有命中则返回200状态码。
优势:
劣势:
Last-Modified
是以秒级为记录的,如果资源在1秒内改变的话,Last-Modified
是无感的。ETag
是 实体标签(Entity Tag) 的缩写。请求数据成功后, 服务端返回数据时响应头会携带 Etag
,他是根据文件内容生成的一个字符串, 当文件变化时他也会跟着改变,一般都是 hash 生成的,是资源的唯一标识。
If-None-Match
是客户端发起的上次返回的 Etag
值。服务器会将 If-None-Match
的值与自己本地资源的 ETag
的值进行对比。如果相等,则表示未修改,响应 304;反之,则表示修改了,响应 200 状态码,并返回最新数据。整个流程基本上和 (Last-Modified
/ If-Modified-Since
) 很类似。
ETag
主要是用来解决修改时间无法准确区分文件变化的问题。
比如,文件的修改时间是秒级甚至更短的,所以一秒内的新版本是无法区分的,再比如,一个文件定期更新,但有时内容没有变化,用修改时间就会以为发生了变化,发送给客户端以为是新的资源,浪费带宽。使用ETag
就可以精确的识别资源的变动情况,让浏览器更有效地利用缓存。
ETag
还有强弱之分。
强 ETag
要求资源在字节级别必须完全相符,弱 ETag
在值前有个W/
标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变(例如 HTML 里的标签顺序调整,或者多了几个空格)。至于是强还是弱,其实是由服务器自主决定的。弱也可以强,强也可以弱。
优势:
劣势:
ETag
值需要性能损耗。ETag
的算法如果不一样,会导致浏览器从一台服务器上获得页面内容后到另外一台服务器上进行验证时出现ETag
不匹配的情况。客户端也可以控制缓存,客户端是怎么控制的呢?
当你点击浏览器的刷新按钮的时候,实际上,浏览器就在HTTP请求中夹带了Cache-Control:max-age=0
,之前说过,max-age
是生存时间,纪录的是从服务器生成的那一刻的有效期,而浏览器本地的资源,肯定不可能是0,所以当浏览器加上了max-age=0
的时候,每次都会向服务器请求最新的资源。
而当你使用Control+F5强制刷新的时候其实是浏览器发了一个Cache-Control: no-cache
。基本上和max-age=0
差不多一样的效果,但是含义肯定是不一样的,就看服务器要怎么理解和处理不同的字段。
那么什么时候缓存才能派上用场呢?当我们点击浏览器的前进后退按钮的时候,就会直接从缓存中获取数据,另外,重定向的时候,也可能会使用到缓存。那这两类操作有啥区别呢。其实本质上来说,就是前进、后退、跳转这样的操作,浏览器不会私自加上Cache-Control
字段,并且会清空If-Modified-Since
和 If-None-Match
字段,所以就会检查缓存,直接利用之前的资源,不再进行网络通信。
大家还可以自己尝试设置no-store
,no-cache
等字段。当你设置了no-store
属性后,你会发现,哪怕使用浏览器的前进,后退按钮,每次也是重新从服务器获取资源,但是no-cache
和max-age
则会使用缓存。
HTTP 信息头中包含 Cache-Control:no-cache,pragma:no-cache
,或 Cache-Control:max-age=0
等告诉浏览器不用缓存的请求。
HTTP响应头中不包含Cache-Control
/Expires
,也不包含Last-Modified
/ETag
的请求无法被缓存。
POST 请求无法被缓存
我们不仅要缓存代码,还需要更新代码。如果静态资源名字不变,怎么让浏览器既能缓存,又能在有新代码时更新?
最简单的解决方式就是静态资源路径添加一个版本值。
版本不变就走缓存策略,版本变了就加载新资源。如下:
<script src="xx/xx.js?v=24334452"></script>
然而这种处理方式在部署时有问题。
背景:静态资源和页面是分开部署的
无论是先部署页面,还是先部署静态资源,在特定的阶段访问,就会出现页面和js文件不匹配进而报错。
这些问题的本质是以上的部署方式是“覆盖式发布”,解决方式是“非覆盖式发布”。
即用静态资源的文件摘要信息给文件命名,这样每次更新资源不会覆盖原来的资源,先将资源发布上去。
这时候存在两种资源,用户用旧页面访问旧资源,然后再更新页面,用户变成新页面访问新资源,就能做到无缝切换。
简单来说就是给静态文件名加hash值。
那如何实现呢?使用webpack持久化缓存
现在前端代码都用webpack之类的构建工具打包。浏览器有其缓存机制,想要既能缓存又能在部署时没有问题,需要给静态文件名添加hash值。