(: 因为一手文章在掘金,只换了
gif
,剩下的原图有水印,懒得换了,gif 有些模糊,自行脑补。
蓦然回首,那“缓存”却在灯火阑珊处~
Situation(情景)
作为日常的知识复习,重温一下浏览器缓存。
知识不止于眼前的文字,更在当下的实操。
Task(任务)
- 通过查阅权威资料整理关键知识点
- 通过实操,重温缓存场景
Action(动作)
我们可以先来聊一个概念,就是一个东西的出现必然是有意义的。为什么要缓存?
简单讲,节省不必要的流量开支,提升客户端访问速度。
更宏观意义上来讲:
蝴蝶效应:大量网络请求会消耗资源,产生热量,导致全球变暖。
举个例子:当 Client
每次向 Server
请求资源,相同的资源只有首次加载会耗费流量,后面统一从本地缓存中拿。
开始之前先介绍一个概念:新鲜度(Freshness)
摘录自:HTTP 规范
A fresh response is one whose age has not yet exceeded its freshness lifetime. Conversely, a stale response is one where it has.
新鲜响应是指其年龄尚未超过其新鲜期的响应。反之,陈旧的响应是指它已经超过了。
A response’s freshness lifetime is the length of time between its generation by the origin server and its expiration time. An explicit expiration time is the time at which the origin server intends that a stored response can no longer be used by a cache without further validation, whereas a heuristic expiration time is assigned by a cache when no explicit expiration time is available.
响应的保鲜期是指从源服务器生成到过期时间之间的时间长度。明确的过期时间是指源服务器打算让缓存在没有进一步验证的情况下不再使用存储的响应的时间,而启发式过期时间是由缓存在没有明确过期时间的情况下指定的。
确定新鲜度的主要机制是源服务器使用Expires标头字段(第 5.3 节)或 max-age 响应指令(第 5.2.2.8 节)提供未来的明确到期时间。
公式:response_is_fresh = (freshness_lifetime > current_age)
freshness_lifetime
- 如果缓存是共享的并且存在 s-maxage 响应指令(第 5.2.2.9 节),则使用它的值,或者
- 如果存在 max-age 响应指令(第 5.2.2.8 节),请使用它的值,或者
- 如果存在Expires响应头字段(第 5.3 节),则使用其值减去Date响应头字段的值,或者
- 否则,响应中不存在明确的到期时间。启发式的新鲜度生命周期可能适用;请参阅第 4.2.2 节。
current_age
- 由于规范内容过多,并非本文的重点,这里不再做赘述,详细可查看:传送门
总结一句话:通过 Cache-Control
和 Expires
,原始服务器可以对资源定义其保质期。在保质期之内,缓存就认为该资源是新鲜的,可以直接传回给客户端,如果过期那么就需要进行再验证。
HTTP 缓存策略:
- 强缓存(
Response Header
中的Expries
和Cache-Control
头) - 协商缓存(
Res & Req Header
中的Last-Modify/If-Modify-Since
和Etag/If-None-Match
)
下面展开讲一讲
Expries(需要固定格式的绝对日期)
表示缓存过期的时间,相对于主机而言的绝对时间。
先简单用 nest
随便搭建个静态资源服务器,并设置 Expires
头:
app.useStaticAssets(join(__dirname, '..', 'public'), {
setHeaders: (res) => {
res.setHeader(
'Expires',
dayjs('2022-08-19 13:00:15').add(15, 'seconds').toDate(),
);
res.setHeader('Cache-Control', 'private');
res.setHeader('Date', Date());
},
lastModified: false,
etag: false,
});
来个动画
首次请求时,浏览器进行了数据缓存,再次请求就不再向服务器发送请求,而是直接从 disk cache
中拿了。
Cache-Control
下面是头可选的几个值:
Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate
先在静态资源服务器的 Response
上塞个 Cache-Control
头:
app.useStaticAssets(join(__dirname, '..', 'public'), {
setHeaders: (res) => {
// res.setHeader(
// 'Expires',
// dayjs('2022-08-19 13:00:15').add(15, 'seconds').toDate(),
// );
res.setHeader('Cache-Control', 'max-age=3000');
res.setHeader('Date', Date());
},
lastModified: false,
etag: false,
});
依旧再看动画:
max-age=3000
的意思就是 3000
秒后过期。这个 3000
秒是相对于 Response
的 Date
头而言的。
为了说明根本机时间是无关的,我截图了最新一次请求和我本机时间的截图拼接。
那么如果两个头都存在呢?会做何种决策?实践的结果可能并不能说服我们,一样的思路,先翻规范,后实践证实。
If a response includes a Cache-Control field with the max-age directive (Section 5.2.2.8), a recipient MUST ignore the Expires field. Likewise, if a response includes the s-maxage directive (Section 5.2.2.9), a shared cache recipient MUST ignore the Expires field. In both these cases, the value in Expires is only intended for recipients that have not yet implemented the Cache-Control field.
如果一个响应包括一个带有max-age指令的Cache-Control字段(第5.2.2.8节),接收者必须忽略 Expires 字段。同样,如果一个响应包括 s-maxage 指令(第5.2.2.9节),共享缓存接收者必须忽略 Expires 字段。在这两种情况下,Expires 中的值只针对还没有实现 Cache-Control 字段的接收者。
规范结论:如果同时存在,那么以 max-age
为准。
下面,我们同时设置这两个头:
app.useStaticAssets(join(__dirname, '..', 'public'), {
setHeaders: (res) => {
res.setHeader(
'Expires',
dayjs('2022-08-20 13:00:15').add(15, 'seconds').toDate(),
);
res.setHeader('Cache-Control', 'public,max-age=0');
res.setHeader('Date', Date());
},
lastModified: false,
etag: false,
});
接着看动画:
可以看到,每次请求都是原始的 4.8m
。小提示:max-age=0
等同于 no-cache
。
小 Tip:每次请求如何要求浏览器不拿内存呢?前提是不使用 Response Header
做文章
- 在
Dev Tools
里开启Disable cache
- 在
- 借助
Chrome
插件,Requesty
来进行手动设置
- 借助
这里可以再提一个概念:重新加载和强制重新加载,见MDN
重新加载的实现方式:据 MDN
描述:
For recovering from window corruption or updating to the latest version of the resource, browsers provide a reload function for users.
That behavior is also defined in the Fetch standard and can be reproduced in JavaScript by calling
fetch()
with the cache mode set tono-cache
(note thatreload
is not the right mode for this case):
上文借助 chrome
插件就是用了后者去实现重新加载的。
强制重新加载的实现方式:
开启
Disable cache
在浏览器中操作
接着上面的内容,我们来继续扩展讲讲剩下的几个值,应用到 Cache-Control
上有什么效果。这里只列举常用的。
private
/public
private
代表私有缓存,例如近用户浏览器本地缓存等;public
代表共享缓存;
—- 来自Stackoverflow
no-cache
- 要求缓存重新验证与源服务器的每个请求。
no-store
- 禁止缓存
到这里,强缓存就结束了。
那么思考一个问题,缓存失效了,它还能用吗?我想“续命”再用怎么办?跟服务端 token
续命相仿。带着这个问题,我们开始协商缓存的介绍。
同样的形式,跟上文一样,我们开始从 http 1.0
的标准到 http 1.1
。
Last-Modified
& If-Modified-Since
Last-Modified: 2022-08-19 22:00:00
Cache-Control: max-age=20
当请求资源后,浏览器收到这个相应头后开始缓存,并且标注“这个资源”最后更改的时间是:2022-08-19 22:00:00
,并且在 20
秒后过期。
等这 20s
过期后,浏览器带着客户端的 If-Modified-Since
头说:老弟,我帮你问问,服务器的资源有没有变更过,问完之后,发现就没变过,返回 304
,继续从缓存里拿。
如果是新的,那么就执行下类似 no-cache
的操作,给到客户端最新的资源。
我们开始操作静态资源服务器的设置:
app.useStaticAssets(join(__dirname, '..', 'public'), {
setHeaders: (res, path, stat) => {
// res.setHeader(
// 'Expires',
// dayjs('2022-08-20 13:00:15').add(15, 'seconds').toDate(),
// );
// res.setHeader('Cache-Control', 'private');
res.setHeader('Last-Modified', dayjs('2022-08-19 19:38:15').toDate());
res.setHeader('Date', Date());
},
// lastModified: false,
etag: false,
redirect: false,
index: false,
maxAge: 2000,
});
nest
中 max-age
是以 ms
为单位的,也就是在 2s
后进行对比:Last-Modified
& If-Modified-Since
。
开始看动画:
可以看到,第一次请求是 4.8MB
, 第二次是 290B
(因为 2
秒很快,所以已经过了缓存,并且比对完了,此时的 200
等同于 304
效果),可以仔细观察下第5次,恰好命中了缓存,所以是 disk cache
。
Etag
& If-None-Match
细心的同学可能已经发现个问题,根据修改时间去做对比很容易漏掉一种场景:就是打开了文件又保存了一下,没做任何变更,但是此时,计算机已经给文件打上了最新的时间
。
这个时候,后面的这一对头就派上用场了。
所谓:“年年岁岁花相似,岁岁年年人不同”。本质来讲这岁岁年年其实还是这个人,但是花可就不是了,时间也不是。
同理,Etag
是根据内容产生的 hash
,跟一个人的基因序列
一样,除非基因序列
变了,那这个人就变了,至于变成啥样子,咱也不知道。所以内容变了,Etag
也就变了。
跟上面的标签对是一样的,缓存前,本地记下原始的 Etag
,等到缓存过期后,把原始的 Etag
赋给 Request
的 If-None-Match
头。带着这个头,去服务端对比。
静态服务器代码:
app.useStaticAssets(join(__dirname, '..', 'public'), {
setHeaders: (res, path, stat) => {
// res.setHeader(
// 'Expires',
// dayjs('2022-08-20 13:00:15').add(15, 'seconds').toDate(),
// );
// res.setHeader('Cache-Control', 'private');
// res.setHeader('Last-Modified', dayjs('2022-08-19 19:38:15').toDate());
res.setHeader('Date', Date());
},
lastModified: false,
// etag: false,
redirect: false,
index: false,
maxAge: 2000,
});
继续看动画:
还是老对话了,客户端缓存过期了,浏览器带着原始 Etag
(If-None-Match
)去请求服务器,服务器发现 Etag
没变过,说:“浏览器,你可以继续用原来的缓存”,于是,客户端此时拿到的资源还是原来缓存里的。
Result(结论)
HTTP 缓存体系
都看到这里了,剩下的总结就交给读者吧,这里只列举大纲,实际详细的内容上文已经有了。
- 存储策略
- 过期策略
- 对比策略
参考资料
最后,本次文章的内容到这里就全部结束了,我是不换
,浪子回头金不换的那个不换哟~,如果本文对你有所帮助,可以帮忙点个小赞,如果有不对的地方,可以评论帮我斧正。