存优点
通常所说的Web缓存指的是可以自动保存常见http请求副本的http设备。对于前端开发者来说,浏览器充当了重要角色。除此外常见的还有各种各样的代理服务器也可以做缓存。当Web请求到达缓存时,缓存从本地副本`本地副本`中提取这个副本内容而不需要经过服务器。这带来了以下优点:
缓存减少了冗余的数据传输,节省流量
缓存缓解了带宽瓶颈问题。不需要更多的带宽就能更快加载页面
缓存缓解了瞬间拥塞,降低了对原始服务器的要求。
缓存降低了距离延时, 因为从较远的地方加载页面会更慢一些。
缓存种类
缓存可以是单个用户专用的,也可以是多个用户共享的。专用缓存被称为私有缓存,共享的缓存被称为公有缓存。
私有缓存
私有缓存只针对专有用户,所以不需要很大空间,廉价。Web浏览器中有内建的私有缓存——大多数浏览器都会将常用资源缓存在你的个人电脑的磁盘和内存中。如Chrome浏览器的缓存存放位置就在:C:\Users\Your_Account\AppData\Local\Google\Chrome\User Data\Default中的Cache文件夹和Media Cache文件夹。
公有缓存
公有缓存是特殊的共享代理服务器,被称为缓存代理服务器或代理缓存(反向代理的一种用途)。公有缓存会接受来自多个用户的访问,所以通过它能够更好的减少冗余流量。
下图中每个客户端都会重复的向服务器访问一个资源(此时还不在私有缓存中),这样它会多次访问服务器,增加服务器压力。而使用共享的公有缓存时,缓存只需要从服务器取一次,以后不用再经过服务器,能够显著减轻服务器压力。
事实上在实际应用中通常采用层次化的公有缓存,基本思想是在靠近客户端的地方使用小型廉价缓存,而更高层次中,则逐步采用更大、功能更强的缓存在装载多用户共享的资源。
缓存处理流程
而对于前端开发者来说,我们主要跟浏览器中的缓存打交道,所以上图流程简化为:
下面这张图展示了某一网站,对不同资源的请求结果,其中可以看到有的资源直接从缓存中读取,有的资源跟服务器进行了再验证,有的资源重新从服务器端获取。
注意,我们讨论的所有关于缓存资源的问题,都仅仅针对GET请求。而对于POST, DELETE, PUT这类行为性操作通常不做任何缓存
新鲜度限值
HTTP通过缓存将服务器资源的副本保留一段时间,这段时间称为新鲜度限值。这在一段时间内请求相同资源不会再通过服务器。HTTP协议中Cache-Control 和 Expires可以用来设置新鲜度的限值,前者是HTTP1.1中新增的响应头,后者是HTTP1.0中的响应头。二者所做的事时都是相同的,但由于Cache-Control使用的是相对时间,而Expires可能存在客户端与服务器端时间不一样的问题,所以我们更倾向于选择Cache-Control。
Cache-Control
下面我们来看看Cache-Control都可以设置哪些属性值:
max-age(单位为s)指定设置缓存最大的有效时间,定义的是时间长短。当浏览器向服务器发送请求后,在max-age这段时间里浏览器就不会再向服务器发送请求了。
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta http-equiv="X-UA-Compatible" content="IE=EDGE" />
<title>Web Cache</title>
<link rel="shortcut icon" href="./shortcut.png">
<script>
</script>
</head>
<body class="claro">
<img src="./cache.png">
</body>
</html>
var http = require('http');
var fs = require('fs');
http.createServer(function(req, res) {
if (req.url === '/' || req.url === '' || req.url === '/index.html') {
fs.readFile('./index.html', function(err, file) {
console.log(req.url)
//对主文档设置缓存,无效果
res.setHeader('Cache-Control', "no-cache, max-age=" + 5);
res.setHeader('Content-Type', 'text/html');
res.writeHead('200', "OK");
res.end(file);
});
}
if (req.url === '/cache.png') {
fs.readFile('./cache.png', function(err, file) {
res.setHeader('Cache-Control', "max-age=" + 5);//缓存五秒
res.setHeader('Content-Type', 'images/png');
res.writeHead('200', "Not Modified");
res.end(file);
});
}
}).listen(8888)
当在5秒内第二次访问页面时,浏览器会直接从缓存中取得资源
public 指定响应可以在代理缓存中被缓存,于是可以被多用户共享。如果没有明确指定private,则默认为public。
private 响应只能在私有缓存中被缓存,不能放在代理缓存上。对一些用户信息敏感的资源,通常需要设置为private。
no-cache 表示必须先与服务器确认资源是否被更改过(依靠If-None-Match和Etag),然后再决定是否使用本地缓存。
如果上文中关于cache.png的处理改成下面这样,则每次访问页面,浏览器都需要先去服务器端验证资源有没有被更改。
fs.readFile('./cache.png', function(err, file) {
console.log(req.headers);
console.log(req.url)
if (!req.headers['if-none-match']) {
res.setHeader('Cache-Control', "no-cache, max-age=" + 5);
res.setHeader('Content-Type', 'images/png');
res.setHeader('Etag', "ffff");
res.writeHead('200', "Not Modified");
res.end(file);
} else {
if (req.headers['if-none-match'] === 'ffff') {
res.writeHead('304', "Not Modified");
res.end();
} else {
res.setHeader('Cache-Control', "max-age=" + 5);
res.setHeader('Content-Type', 'images/png');
res.setHeader('Etag', "ffff");
res.writeHead('200', "Not Modified");
res.end(file);
}
}
});
no-store 绝对禁止缓存任何资源,也就是说每次用户请求资源时,都会向服务器发送一个请求,每次都会下载完整的资源。通常用于机密性资源。
关于Cache-Control的使用,见下面这张图(来自大额)
客户端的新鲜度限值
Cache-Control不仅仅可以在响应头中设置,还可以在请求头中设置。浏览器通过请求头中设置Cache-Control可以决定是否从缓存中读取资源。*这也是为什么有时候点击浏览器刷新按钮和在地址栏回车,在NetWork模块中看到完全不同的结果*
Expires
不推荐使用Expires,它指定的是具体的过期日期而不是秒数。因为很多服务器跟客户端存在时钟不一致的情况,所以最好还是使用Cache-Control.
服务器再验证
浏览器或代理缓存中缓存的资源过期了,并不意味着它和原始服务器上的资源有实际的差异,仅仅意味着到了要进行核对的时间了。这种情况被称为服务器再验证。
如果资源发生变化,则需要取得新的资源,并在缓存中替换旧资源。
如果资源没有发生变化,缓存只需要获取新的响应头,和一个新的过期时间,对缓存中的资源过期时间进行更新即可。 HTTP1.1推荐使用的验证方式是If-None-Match/Etag,在HTTP1.0中则使用If-Modified-Since/Last-Modified。
Etag与If-None-Match
根据实体内容生成一段hash字符串,标识资源的状态,由服务端产生。浏览器会将这串字符串传回服务器,验证资源是否已经修改,如果没有修改,过程如下(图片来自浅谈Web缓存):
上文的demo中我们见到过服务器端如何验证Etag:
由于Etag有服务器构造,所以在集群环境中一定要保证Etag的唯一性
If-Modified-Since与Last-Modified
这两个是HTTP1.0中用来验证资源是否过期的请求/响应头,这两个头部都是日期,验证过程与Etag类似,这里不详细介绍。使用这两个头部来验证资源是否更新时,存在以下问题:
有些文档资源周期性的被重写,但实际内容没有改变。此时文件元数据中会显示文件最近的修改日期与If-Modified-Since不相同,导致不必要的响应。
有些文档资源被修改了,但修改内容并不重要,不需要所有的缓存都更新(比如代码注释)
关于缓存的更新问题,请大家看看这里张云龙的回答,本文就不详细展开了。
本文demo代码如下:
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta http-equiv="X-UA-Compatible" content="IE=EDGE" />
<title>Web Cache</title>
<link rel="shortcut icon" href="./shortcut.png">
<script>
</script>
</head>
<body class="claro">
<img src="./cache.png">
</body>
</html>
var http = require('http');
var fs = require('fs');
http.createServer(function(req, res) {
if (req.url === '/' || req.url === '' || req.url === '/index.html') {
fs.readFile('./index.html', function(err, file) {
console.log(req.url)
//对主文档设置缓存,无效果
res.setHeader('Cache-Control', "no-cache, max-age=" + 5);
res.setHeader('Content-Type', 'text/html');
res.writeHead('200', "OK");
res.end(file);
});
}
if (req.url === '/shortcut.png') {
fs.readFile('./shortcut.png', function(err, file) {
console.log(req.url)
res.setHeader('Content-Type', 'images/png');
res.writeHead('200', "OK");
res.end(file);
})
}
if (req.url === '/cache.png') {
fs.readFile('./cache.png', function(err, file) {
console.log(req.headers);
console.log(req.url)
if (!req.headers['if-none-match']) {
res.setHeader('Cache-Control', "max-age=" + 5);
res.setHeader('Content-Type', 'images/png');
res.setHeader('Etag', "ffff");
res.writeHead('200', "Not Modified");
res.end(file);
} else {
if (req.headers['if-none-match'] === 'ffff') {
res.writeHead('304', "Not Modified");
res.end();
} else {
res.setHeader('Cache-Control', "max-age=" + 5);
res.setHeader('Content-Type', 'images/png');
res.setHeader('Etag', "ffff");
res.writeHead('200', "Not Modified");
res.end(file);
}
}
});
}
}).listen(8888)
更多深度技术内容,请关注云栖社区微信公众号:yunqiinsight。
家都知道缓存的英文叫做 cache。但我发现一个有趣的现象:这个单词在不同人的口中有不同的读音。为了全面了解缓存,我们得先从读音开始,这样才能够在和其他同事(例如 PM)交(zhuāng)流(bī)时体现自己的修(bī)养(gé)。cache 的发音是 /kæʃ/(同 cash),这也是一个广泛认可的发音。
扯了些没用的,我们先进入定义环节:什么是前端缓存?与之相对的什么又是后端缓存?
基本的网络请求就是三个步骤:请求,处理,响应。
后端缓存主要集中于“处理”步骤,通过保留数据库连接,存储处理结果等方式缩短处理时间,尽快进入“响应”步骤。当然这不在本文的讨论范围之内。
而前端缓存则可以在剩下的两步:“请求”和“响应”中进行。在“请求”步骤中,浏览器也可以通过存储结果的方式直接使用资源,直接省去了发送请求;而“响应”步骤需要浏览器和服务器共同配合,通过减少响应内容来缩短传输时间。这些都会在下面进行讨论。
本文主要包含
我看过的大部分讨论缓存的文章会直接从 HTTP 协议头中的缓存字段开始,例如 Cache-Control, ETag, max-age 等。但偶尔也会听到别人讨论 memory cache, disk cache 等。那这两种分类体系究竟有何关联?是否有交叉?(我个人认为这是本文的最大价值所在,因为在写之前我自己也是被两种分类体系搞的一团糟)
实际上,HTTP 协议头的那些字段,都属于 disk cache 的范畴,是几个缓存位置的其中之一。因此本着从全局到局部的原则,我们应当先从缓存位置开始讨论。等讲到 disk cache 时,才会详细讲述这些协议头的字段及其作用。
我们可以在 Chrome 的开发者工具中,Network -> Size 一列看到一个请求最终的处理方式:如果是大小 (多少 K, 多少 M 等) 就表示是网络请求,否则会列出 from memory cache, from disk cache 和 from ServiceWorker。
它们的优先级是:(由上到下寻找,找到即返回;找不到则继续)
memory cache
memory cache 是内存中的缓存,(与之相对 disk cache 就是硬盘上的缓存)。按照操作系统的常理:先读内存,再读硬盘。disk cache 将在后面介绍 (因为它的优先级更低一些),这里先讨论 memory cache。
几乎所有的网络请求资源都会被浏览器自动加入到 memory cache 中。但是也正因为数量很大但是浏览器占用的内存不能无限扩大这样两个因素,memory cache 注定只能是个“短期存储”。常规情况下,浏览器的 TAB 关闭后该次浏览的 memory cache 便告失效 (为了给其他 TAB 腾出位置)。而如果极端情况下 (例如一个页面的缓存就占用了超级多的内存),那可能在 TAB 没关闭之前,排在前面的缓存就已经失效了。
刚才提过,几乎所有的请求资源 都能进入 memory cache,这里细分一下主要有两块:
memory cache 机制保证了一个页面中如果有两个相同的请求 (例如两个 src 相同的 <img>,两个 href 相同的 <link>)都实际只会被请求最多一次,避免浪费。
不过在匹配缓存时,除了匹配完全相同的 URL 之外,还会比对他们的类型,CORS 中的域名规则等。因此一个作为脚本 (script) 类型被缓存的资源是不能用在图片 (image) 类型的请求中的,即便他们 src 相等。
在从 memory cache 获取缓存内容时,浏览器会忽视例如 max-age=0, no-cache 等头部配置。例如页面上存在几个相同 src 的图片,即便它们可能被设置为不缓存,但依然会从 memory cache 中读取。这是因为 memory cache 只是短期使用,大部分情况生命周期只有一次浏览而已。而 max-age=0 在语义上普遍被解读为“不要在下次浏览时使用”,所以和 memory cache 并不冲突。
但如果站长是真心不想让一个资源进入缓存,就连短期也不行,那就需要使用 no-store。存在这个头部配置的话,即便是 memory cache 也不会存储,自然也不会从中读取了。(后面的第二个示例有关于这点的体现)
disk cache
disk cache 也叫 HTTP cache,顾名思义是存储在硬盘上的缓存,因此它是持久存储的,是实际存在于文件系统中的。而且它允许相同的资源在跨会话,甚至跨站点的情况下使用,例如两个站点都使用了同一张图片。
disk cache 会严格根据 HTTP 头信息中的各类字段来判定哪些资源可以缓存,哪些资源不可以缓存;哪些资源是仍然可用的,哪些资源是过时需要重新请求的。当命中缓存之后,浏览器会从硬盘中读取资源,虽然比起从内存中读取慢了一些,但比起网络请求还是快了不少的。绝大部分的缓存都来自 disk cache。
关于 HTTP 的协议头中的缓存字段,我们会在稍后进行详细讨论。
凡是持久性存储都会面临容量增长的问题,disk cache 也不例外。在浏览器自动清理时,会有神秘的算法去把“最老的”或者“最可能过时的”资源删除,因此是一个一个删除的。不过每个浏览器识别“最老的”和“最可能过时的”资源的算法不尽相同,可能也是它们差异性的体现。
Service Worker
上述的缓存策略以及缓存/读取/失效的动作都是由浏览器内部判断 & 进行的,我们只能设置响应头的某些字段来告诉浏览器,而不能自己操作。举个生活中去银行存/取钱的例子来说,你只能告诉银行职员,我要存/取多少钱,然后把由他们会经过一系列的记录和手续之后,把钱放到金库中去,或者从金库中取出钱来交给你。
但 Service Worker 的出现,给予了我们另外一种更加灵活,更加直接的操作方式。依然以存/取钱为例,我们现在可以绕开银行职员,自己走到金库前(当然是有别于上述金库的一个单独的小金库),自己把钱放进去或者取出来。因此我们可以选择放哪些钱(缓存哪些文件),什么情况把钱取出来(路由匹配规则),取哪些钱出来(缓存匹配并返回)。当然现实中银行没有给我们开放这样的服务。
Service Worker 能够操作的缓存是有别于浏览器内部的 memory cache 或者 disk cache 的。我们可以从 Chrome 的 F12 中,Application -> Cache Storage 找到这个单独的“小金库”。除了位置不同之外,这个缓存是永久性的,即关闭 TAB 或者浏览器,下次打开依然还在(而 memory cache 不是)。有两种情况会导致这个缓存中的资源被清除:手动调用 API cache.delete(resource) 或者容量超过限制,被浏览器全部清空。
如果 Service Worker 没能命中缓存,一般情况会使用 fetch() 方法继续获取资源。这时候,浏览器就去 memory cache 或者 disk cache 进行下一次找缓存的工作了。注意:经过 Service Worker 的 fetch() 方法获取的资源,即便它并没有命中 Service Worker 缓存,甚至实际走了网络请求,也会标注为 from ServiceWorker。这个情况在后面的第三个示例中有所体现。
发送网络请求
如果一个请求在上述 3 个位置都没有找到缓存,那么浏览器会正式发送网络请求去获取内容。之后容易想到,为了提升之后请求的缓存命中率,自然要把这个资源添加到缓存中去。具体来说:
memory cache 是浏览器为了加快读取缓存速度而进行的自身的优化行为,不受开发者控制,也不受 HTTP 协议头的约束,算是一个黑盒。Service Worker 是由开发者编写的额外的脚本,且缓存位置独立,出现也较晚,使用还不算太广泛。所以我们平时最为熟悉的其实是 disk cache,也叫 HTTP cache (因为不像 memory cache,它遵守 HTTP 协议头中的字段)。平时所说的强制缓存,对比缓存,以及 Cache-Control 等,也都归于此类。
强制缓存 (也叫强缓存)
强制缓存的含义是,当客户端请求后,会先访问缓存数据库看缓存是否存在。如果存在则直接返回;不存在则请求真的服务器,响应后再写入缓存数据库。
强制缓存直接减少请求数,是提升最大的缓存策略。 它的优化覆盖了文章开头提到过的请求数据的全部三个步骤。如果考虑使用缓存来优化网页性能的话,强制缓存应该是首先被考虑的。
可以造成强制缓存的字段是 Cache-control 和 Expires。
Expires
这是 HTTP 1.0 的字段,表示缓存到期时间,是一个绝对的时间 (当前时间+缓存时间),如
Expires: Thu, 10 Nov 2017 08:45:11 GMT
在响应消息头中,设置这个字段之后,就可以告诉浏览器,在未过期之前不需要再次请求。
但是,这个字段设置时有两个缺点:
Cache-control
已知Expires的缺点之后,在HTTP/1.1中,增加了一个字段Cache-control,该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务器发送请求
这两者的区别就是前者是绝对时间,而后者是相对时间。如下:
Cache-control: max-age=2592000
下面列举一些 Cache-control 字段常用的值:(完整的列表可以查看 MDN)
这些值可以混合使用,例如 Cache-control:public, max-age=2592000。在混合使用时,它们的优先级如下图:
这里有一个疑问:max-age=0 和 no-cache 等价吗?从规范的字面意思来说,max-age 到期是 应该(SHOULD) 重新验证,而 no-cache 是 必须(MUST) 重新验证。但实际情况以浏览器实现为准,大部分情况他们俩的行为还是一致的。(如果是 max-age=0, must-revalidate 就和 no-cache 等价了)
顺带一提,在 HTTP/1.1 之前,如果想使用 no-cache,通常是使用 Pragma 字段,如 Pragma: no-cache(这也是 Pragma 字段唯一的取值)。但是这个字段只是浏览器约定俗成的实现,并没有确切规范,因此缺乏可靠性。它应该只作为一个兼容字段出现,在当前的网络环境下其实用处已经很小。
总结一下,自从 HTTP/1.1 开始,Expires 逐渐被 Cache-control 取代。Cache-control 是一个相对时间,即使客户端时间发生改变,相对时间也不会随之改变,这样可以保持服务器和客户端的时间一致性。而且 Cache-control 的可配置性比较强大。
Cache-control 的优先级高于 Expires,为了兼容 HTTP/1.0 和 HTTP/1.1,实际项目中两个字段我们都会设置。
对比缓存 (也叫协商缓存)
当强制缓存失效(超过规定时间)时,就需要使用对比缓存,由服务器决定缓存内容是否失效。
流程上说,浏览器先请求缓存数据库,返回一个缓存标识。之后浏览器拿这个标识和服务器通讯。如果缓存未失效,则返回 HTTP 状态码 304 表示继续使用,于是客户端继续使用缓存;如果失效,则返回新的数据和缓存规则,浏览器响应数据后,再把规则写入到缓存数据库。
对比缓存在请求数上和没有缓存是一致的,但如果是 304 的话,返回的仅仅是一个状态码而已,并没有实际的文件内容,因此 在响应体体积上的节省是它的优化点。它的优化覆盖了文章开头提到过的请求数据的三个步骤中的最后一个:“响应”。通过减少响应体体积,来缩短网络传输时间。所以和强制缓存相比提升幅度较小,但总比没有缓存好。
对比缓存是可以和强制缓存一起使用的,作为在强制缓存失效后的一种后备方案。实际项目中他们也的确经常一同出现。
对比缓存有 2 组字段(不是两个):
Last-Modified & If-Modified-Since
Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
但是他还是有一定缺陷的:
Etag & If-None-Match
为了解决上述问题,出现了一组新的字段 Etag 和 If-None-Match
Etag 存储的是文件的特殊标识(一般都是 hash 生成的),服务器存储着文件的 Etag 字段。之后的流程和 Last-Modified 一致,只是 Last-Modified 字段和它所表示的更新时间改变成了 Etag 字段和它所表示的文件 hash,把 If-Modified-Since 变成了 If-None-Match。服务器同样进行比较,命中返回 304, 不命中返回新资源和 200。
Etag 的优先级高于 Last-Modified
当浏览器要请求资源时
光看原理不免枯燥。我们编写一些简单的网页,通过案例来深刻理解上面的那些原理。
1. memory cache & disk cache
我们写一个简单的 index.html,然后引用 3 种资源,分别是 index.js, index.css 和 mashroom.jpg。
我们给这三种资源都设置上 Cache-control: max-age=86400,表示强制缓存 24 小时。以下截图全部使用 Chrome 的隐身模式。
1、首次请求
毫无意外的全部走网络请求,因为什么缓存都还没有。
2、再次请求 (F5)
第二次请求,三个请求都来自 memory cache。因为我们没有关闭 TAB,所以浏览器把缓存的应用加到了 memory cache。(耗时 0ms,也就是 1ms 以内)
3、关闭 TAB,打开新 TAB 并再次请求
因为关闭了 TAB,memory cache 也随之清空。但是 disk cache 是持久的,于是所有资源来自 disk cache。(大约耗时 3ms,因为文件有点小)
2. no-cache & no-store
我们在 index.html 里面一些代码,完成两个目标:
1、当把服务器响应设置为 Cache-Control: no-cache 时,我们发现打开页面之后,三种资源都只被请求 1 次。
这说明两个问题:
总体来说,如上面原理所述,no-cache 从语义上表示下次请求不要直接使用缓存而需要比对,并不对本次请求进行限制。因此浏览器在处理当前页面时,可以放心使用缓存。
2、当把服务器响应设置为 Cache-Control: no-store 时,情况发生了变化,三种资源都被请求了 2 次。而图片因为还多一次异步请求,总计 3 次。(红框中的都是那一次异步请求)
这同样说明:
3、Service Worker & memory (disk) cache
我们尝试把 Service Worker 也加入进去。我们编写一个 serviceWorker.js,并编写如下内容:(主要是预缓存 3 个资源,并在实际请求时匹配缓存并返回)
注册 SW 的代码这里就不赘述了。此外我们还给服务器设置 Cache-Control: max-age=86400 来开启 disk cache。我们的目的是看看两者的优先级。
1、当我们首次访问时,会看到常规请求之外,浏览器(确切地说是 Service Worker)额外发出了 3 个请求。这来自预缓存的代码。
2、第二次访问(无论关闭 TAB 重新打开,还是直接按 F5 刷新)都能看到所有的请求标记为 from SerciceWorker。
from ServiceWorker 只表示请求通过了 Service Worker,至于到底是命中了缓存,还是继续 fetch() 方法光看这一条记录其实无从知晓。因此我们还得配合后续的 Network 记录来看。因为之后没有额外的请求了,因此判定是命中了缓存。
从服务器的日志也能很明显地看到,3 个资源都没有被重新请求,即命中了 Service Worker 内部的缓存。
3、如果修改 serviceWorker.js 的 fetch 事件监听代码,改为如下:
// 这个也叫做 NetworkOnly 的缓存策略。 self.addEventListener('fetch', e => { return e.respondWith(fetch(e.request)) })
可以发现在后续访问时的效果和修改前是 完全一致的。(即 Network 仅有标记为 from ServiceWorker 的几个请求,而服务器也不打印 3 个资源的访问日志)
很明显 Service Worker 这层并没有去读取自己的缓存,而是直接使用 fetch() 进行请求。所以此时其实是 Cache-Control: max-age=86400 的设置起了作用,也就是 memory/disk cache。但具体是 memory 还是 disk 这个只有浏览器自己知道了,因为它并没有显式的告诉我们。(个人猜测是 memory,因为不论从耗时 0ms 还是从不关闭 TAB 来看,都更像是 memory cache)
所谓浏览器的行为,指的就是用户在浏览器如何操作时,会触发怎样的缓存策略。主要有 3 种:
了解了缓存的原理,我们可能更加关心如何在实际项目中使用它们,才能更好的让用户缩短加载时间,节约流量等。这里有几个常用的模式,供大家参考
模式 1:不常变化的资源
Cache-Control: max-age=31536000
通常在处理这类资源资源时,给它们的 Cache-Control 配置一个很大的 max-age=31536000 (一年),这样浏览器之后请求相同的 URL 会命中强制缓存。而为了解决更新的问题,就需要在文件名(或者路径)中添加 hash, 版本号等动态字符,之后更改动态字符,达到更改引用 URL 的目的,从而让之前的强制缓存失效 (其实并未立即失效,只是不再使用了而已)。
在线提供的类库 (如 jquery-3.3.1.min.js, lodash.min.js 等) 均采用这个模式。如果配置中还增加 public 的话,CDN 也可以缓存起来,效果拔群。
这个模式的一个变体是在引用 URL 后面添加参数 (例如 ?v=xxx 或者 ?_=xxx),这样就不必在文件名或者路径中包含动态参数,满足某些完美主义者的喜好。在项目每次构建时,更新额外的参数 (例如设置为构建时的当前时间),则能保证每次构建后总能让浏览器请求最新的内容。
特别注意: 在处理 Service Worker 时,对待 sw-register.js(注册 Service Worker) 和 serviceWorker.js (Service Worker 本身) 需要格外的谨慎。如果这两个文件也使用这种模式,你必须多多考虑日后可能的更新及对策。
模式 2:经常变化的资源
Cache-Control: no-cache
这里的资源不单单指静态资源,也可能是网页资源,例如博客文章。这类资源的特点是:URL 不能变化,但内容可以(且经常)变化。我们可以设置 Cache-Control: no-cache 来迫使浏览器每次请求都必须找服务器验证资源是否有效。
既然提到了验证,就必须 ETag 或者 Last-Modified 出场。这些字段都会由专门处理静态资源的常用类库(例如 koa-static)自动添加,无需开发者过多关心。
也正如上文中提到协商缓存那样,这种模式下,节省的并不是请求数,而是请求体的大小。所以它的优化效果不如模式 1 来的显著。
模式 3:非常危险的模式 1 和 2 的结合 (反例)
Cache-Control: max-age=600, must-revalidate
不知道是否有开发者从模式 1 和 2 获得一些启发:模式 2 中,设置了 no-cache,相当于 max-age=0, must-revalidate。我的应用时效性没有那么强,但又不想做过于长久的强制缓存,我能不能配置例如 max-age=600, must-revalidate 这样折中的设置呢?
表面上看这很美好:资源可以缓存 10 分钟,10 分钟内读取缓存,10 分钟后和服务器进行一次验证,集两种模式之大成,但实际线上暗存风险。因为上面提过,浏览器的缓存有自动清理机制,开发者并不能控制。
举个例子:当我们有 3 种资源: index.html, index.js, index.css。我们对这 3 者进行上述配置之后,假设在某次访问时,index.js 已经被缓存清理而不存在,但 index.html, index.css 仍然存在于缓存中。这时候浏览器会向服务器请求新的 index.js,然后配上老的 index.html, index.css 展现给用户。这其中的风险显而易见:不同版本的资源组合在一起,报错是极有可能的结局。
除了自动清理引发问题,不同资源的请求时间不同也能导致问题。例如 A 页面请求的是 A.js 和 all.css,而 B 页面是 B.js 和 all.css。如果我们以 A -> B 的顺序访问页面,势必导致 all.css 的缓存时间早于 B.js。那么以后访问 B 页面就同样存在资源版本失配的隐患。
有开发者朋友(wd2010)在知乎的评论区提了一个很好的问题:
如果我不使用must-revalidate,只是Cache-Control: max-age=600,浏览器缓存的自动清理机制就不会执行么?如果浏览器缓存的自动清理机制执行的话那后续的index.js被清掉的所引发的情况都是一样的呀!
这个问题涉及几个小点,需要补充说明一下:
1、'max-age=600' 和 'max-age=600,must-revalidate' 有什么区别?
没有区别。在列出 max-age 了之后,must-revalidate 是否列出效果相同,浏览器都会在超过 max-age 之后进行校验,验证缓存是否可用。
在 HTTP 的规范中,只阐述了 must-revalidate 的作用,却没有阐述不列出 must-revalidate 时,浏览器应该如何解决缓存过期的问题,因此这其实是浏览器实现时的自主决策。(可能有少数浏览器选择在源站点无法访问时继续使用过期缓存,但这取决于浏览器自身)
2、那 'max-age=600' 是不是也会引发问题?
是的。问题的出现和是否列出 'must-revalidate' 无关,依然会存在 JS CSS等文件版本失配的问题。因此常规的网站在不同页面需要使用不同的 JS CSS 文件时,如果要使用 max-age 做强缓存,不要设置一个太短的时间。
3、那这类比较短的 max-age 到底能用在哪里呢?
既然版本存在失配的问题,那么要避开这个问题,就有两种方法。
a、整站都使用相同的 JS 和 CSS,即合并后的文件。这个比较适合小型站点,否则可能过于冗余,影响性能。(不过可能还是会因为浏览器自身的清理策略被清理,依然有隐患)
b、资源是独立使用的,并不需要和其他文件配合生效。例如 RSS 就归在此类。
这篇文章有点,不过如果大伙如果能静下心来看完并理解,相信你对前端缓存一定能更上一层楼。
storage类型是来存储名值对儿,是保存在浏览器端的,存储为key-value形式的。
该类型的实例对象有关于存储的方法如下:
(1) clear():删除所有值。
(2) getItem():根据指定的名字(key)获取对应的值。
(3) key(index):获取index位置处的值的名字(key)。
(4) removeItem():删除由name指定的名值对儿。
(5) setItem(name,value): 为指定的name 设置一个对应的值。
(6) Storage.length :返回一个整数,表示存储在 Storage 对象中的数据项数量。这个是 Storage 对象的一个属性,而且是一个 只读 属性
我自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年我花了一个月整理了一份最适合2020年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取。
该类型只能存储字符串,非字符串的数据会在存储之前转换成字符串。
sessionStorage对象存储是web stroage机制的一种,该对象会给每一个给定的源维持一个独立的存储区域,该区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复) sessionStorage对象是绑定在服务器会话的,所以当文件在本地运行的时候是不能跨页面访问的,使用服务器地址访问时才能多页面访问。
在另一个页面打开是找不到这个值的:
开启服务器访问:
再次使用服务器地址访问的时候:
localStorage也是web Stroage存储机制的一种,localStorage对象是根据glocalStorage对象设计出来的简化,glocalStorage的使用是要指定哪些域名可以访问该数据,是通过方括号标记使用该属性来实现的:
//存数据
glocalStorage["baidu.com"].name="刘德华"
//取数据
var name=glocalStorage["baidu.com"].name
听网上说是为了安全性然后在HTML5的规范中用localStorage取代了glocalStorage不能自己设置域名,值使用localStorage对象时就将当前域名加入。规则设计出来就是方便我们的使用的,显然localStorage比之前的使用起来更简单了。
我自己的理解根据该用户不删除数据,数据将会一直保存在磁盘里的属性,可以设置收藏列表,用户爱好等等。
//存数据
glocalStorage.setItem('content', '刘德华');
//取数据
glocalStorage.getItem('content');
sessionStorage:数据只保持在浏览器关闭
localStorage:用户不删除数据,数据将会一直保存在磁盘里
localStorage:只要在相同的协议、相同的主机名、相同的端口下,就能读取和修改到同一份 localStorage 存储的数据。
sessionStorage:除了协议、主机名、端口外,还要求在同一窗口下。
Last-Modified:响应头,资源最新修改时间,由服务器告诉浏览器。 if-Modified-Since:请求头,资源最新修改时间,由浏览器告诉服务器(其实就是上次服务器给的Last-Modified,请求又还给服务器对比),和Last-Modified是一对,它两会在服务器端进行对比。
第一次请求数据
浏览器:服务器服务器,我现在需要一个users.json的文件,你找到了给我,顺便给我文件修改时间!
服务器:行,文件修改时间我给你,到时候文件过期了咱两核对文件修改时间,对得上我就不找了,返回a.txt+Last-Modified。
第二次请求数据 浏览器:服务器服务器,我还需要一个users.json的文件,我把这个文件上次修改时间发你if-Modified-Since,你对比一下文件最近有不有修改!
服务器:ok,我帮你找一下。服务器将if-Modified-Since与Last-Modified做了个对比。
if-Modified-Since 与Last-Modified不相等,服务器查找了最新的users.json,同时再次返回全新的Last-Modified。
if-Modified-Since 与Last-Modified相等,服务器返回状态码304,文件没修改过,你还是用你的本地缓存。
Etag:响应头,由服务器设置告诉浏览器。
if-None-Match:请求头,由浏览器告诉服务器(其实就是上次服务器给的Etag),和Etag是一对,它两也会在服务器端进行对比。
浏览器:服务器服务器,我现在需要一个users.json的文件,你找到了给我!
服务器:你又来了,文件我找给你,我给你一个Etag(文件版本号),到时候你再来找这个资源的时候把这个带上,省的我再找给你,返回a.txt+Etag。
第二次请求数据 浏览器:服务器服务器,我又需要users.json文件了,我把你发给我的Etag放在If-None-Match里面了,你对比一下最近这个文件有不有修改!
服务器:ok,我帮你找一下。服务器将Etag与If-None-Match做了个对比。 Etag与If-None-Match不相等,服务器查找了最新的users.json,同时再次返回全新的Etag(文件版本号)。
Etag与If-None-Match相等,服务器返回状态码304,文件没修改过,你还是用你的本地缓存。
<ul id="users"></ul>
<script>
window.onload = function() {
const users = JSON.parse(localStorage.getItem('usersdata'));
console.log("users", users)
const uluser = document.querySelector('#users');
if (users) {
uluser.innerHTML = users.map(user =>
`
<li>
${user.username}-${user.place}
</li>
`
)
}
fetch('/users')
.then(data => data.json())
.then(data => {
localStorage.setItem('usersdata', JSON.stringify(data)) //需要存储字符串
console.log("users请求")
uluser.innerHTML = data.map(user =>
`
<li>
${user.username}-${user.place}
</li>
`
)
})
}
</script>
const http = require('http');
const fs = require('fs');
const users = require('./users.json')
http.createServer((req, res) => {
let status = fs.statSync('users.json');//获取文件的状态
if (req.url == '/') {
res.writeHead(200, { "Content-Type": "text/html;charset=utf8" })
fs.createReadStream('./users.html')pipe(res);//通过流返回显示页面
} else if (req.url == '/users') {
console.log("用户数据请求")
if (req.headers['if-modified-since']) {
if (req.headers['if-modified-since'] == status.mtime) {//判断请求中的修改时间与文件的修改时间是否相同
console.log("未修改文件返回304")
res.writeHead(304, { "Content-Type": "text/html;charset=utf8" });//304 Not Modified
res.end();
return;
} else {
res.setHeader('Last-Modified', status.mtime);
res.writeHead(200, { "Content-Type": "text/html;charset=utf8" });
console.log('修改了返回修改数据')
res.end(JSON.stringify(users))
return;
}
}
console.log("用户数据第一次")
res.setHeader('Last-Modified', status.mtime);
res.writeHead(200, { "Content-Type": "text/html;charset=utf8" });
res.end(JSON.stringify(users))
} else {
res.end("null")
}
}).listen('8088', () => {
console.log('监听8088')
})
第一次请求数据服务器给我们返回200状态码和Last-Modified,代码中提示的地方提示出用户为第一次数据请求。
文件未修改,第二次请求数据服务端判断出文件未修改,则返回304状态码官方解释304:The HTTP 304 Not Modified client redirection response code indicates that there is no need to retransmit the requested resources.
文件修改,再次访问,服务端检测文件修改时间不一样,则重新返回文件内容
缓存数据可以节省许多资源,对于以上例子还有些缺陷,对于高频访问情况还是不能满足需求,技术就是需要不断的探索。有什么建议或者错误,欢迎指出!
作者:有酒有故事
链接:https://juejin.im/post/6844904164540022797
*请认真填写需求信息,我们会在24小时内与您取得联系。