在前端开发中,性能一直都是被大家所重视的一点,然而判断一个网站的性能最直观的就是看网页打开的速度。其中提高网页反应速度的一个方式就是使用缓存。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。
Web 缓存分为很多种,比如数据库缓存、代理服务器缓存、还有CDN缓存,以及浏览器缓存,我们只选择浏览器缓存来讲解一下:
浏览器缓存的缓存分为强缓存和协商缓存, 大致流程如下
强缓存
当缓存中有从请求响应的资源时,客户端直接从缓训中获取数据。当缓存中没有对应的资源,则直接从服务端拉取数据。
响应头中控制缓存的有两个字段Expires和Cache-Control
Expires
译为到期时间(相对服务器来说)支持时间GTM格式(2020-08-04T16:00:00.000Z)。
"Expires": "2020-08-04T16:00:00.000Z"
表示为该资源一直缓存到2020年8月4日16:00,过期之后直接向服务端重新请求数据。 这种方式存在一个问题,客户端和服务端的时间可能会不一致
Cache-Control
"Cache-Control": "max-age=10"
表示该资源有效缓存时间为10秒,过期之后直接向服务端重新请求数据。
Cache-Control在请求和响应头中都可以使用:具体请看文末
优先级:Cache-Control > Expires, 当两个字段同时出现Expires字段无效
pragma
pragma值有no-cache和no-store两个选项,表示意思同cache-control
"pragma": "no-cache"
三者同时出现时优先级顺序:pragma -> cache-control -> expires
Node服务器实现强缓存
通过node实现服务,理解更容易
1.搭建服务器。
// service.js
const http = require('http');
const fs = require('fs');
http.createServer(function (req, res) {
console.log(`收到响应--- method: ${req.method}`, new Date());
if(req.url === '/') {
fs.readFile('./index.html', function (err, data) {
if (!err) {
res.write(data);
res.end();
}
})
} else {
res.setHeader("Cache-Control","max-age=10");
res.write('Hellow world!');
res.end();
}
}).listen(3000);
console.log('Server running at http://127.0.0.1:3000/');
2.创建客户端文件
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>http缓存</title>
</head>
<body>
<button onclick="loadXMLDoc()">获取数据</button>
</body>
<script>
function loadXMLDoc() {
const ajax = new XMLHttpRequest();
ajax.open("GET", 'http://127.0.0.1:3000/cache', true);
ajax.send();
}
</script>
</html>
点击“获取数据”打开控制台会看到如下现象:
第一次请求(只看GET请方法)该url还没有开始缓存,此时直接向服务端获取数据,当第一次请求响应之后因为响应头中设置了"Cache-Control": "max-age=10"10秒钟之内再次请求就会看到途中第二次请求的结果磁盘缓存。间隔10秒,缓存时间到期之后再发起第三次请求看到又是从服务器中获取。
协商缓存
协商缓存可以叫做对比缓存,当客户端去请求服务端时会带上资源标识符去到服务端进行比对,如果请求的资源和客户端缓存的资源没有任何修改,则客户端直接缓存。
注意:协商缓存和强缓存的重要区别,强缓存下的客户端请求服务器接收不到,协商缓存下的客户端请求服务端时可以接受到只是做在服务端进行数据对比,判断资源是否更新;
协商缓存与强缓存的区别
强缓存都会访问本地缓存直接验证看是否过期,如果没过期直接使用本地缓存,并返回 200。但协商缓存本地缓存会被忽略,会去请求服务器验证资源是否更新,如果没更新才继续使用本地缓存,此时返回的是 304,这就是协商缓存。协商缓存主要包括 Last-Modified 和 Etag。
Last-Modified(最后修改时间)
Last-Modified表示为资源最后一次修改时间,服务端会通过增加Last-Modified响应头来作为缓存标识符, 通常取服务端资源修改的最后时间作为值。- 客户端接受到请求之后下次发送请求,请求头会自动带上
If-Modified-Since字段,值为之前服务端响应头中的Last-Modified的值。 - 服务端拿到请求头
If-Modified-Since的值和当前资源最后修改时间Last-Modified进行比对:
会有以下两种情况
Last-Modified > If-Modified-Since:客户端的缓存不是最新更改的数据,返回新的资源,跟常规的HTTP请求响应的流程一样。Last-Modified = If-Modified-Since:客户端的缓存的资源是最新的,无需再从服务端响应,此时返回304,告诉浏览器直接使用缓存。
Last-Modified: Mon, 10 Aug 2020 10:25:50 GMT
注意:看似完美但是Last-Modified有一个问题,比如获取文件的最后修改状态是获取文件的系统属性中的最后修改时间(node写法:fs.statSync(filename).ctime),当你在改文件里修改一个字段后又删除了这个字段,此时文件的内容没有修改,本应该命中缓存,但是修改时间已经改变了,服务端就会从新响应一次完整的数据。在http1.1中使用Etag来做资源的唯一标识符,只要文件内容没有改动则一定会命中缓存。
Etag(文件唯一标识符)
Etag会基于资源生成一串唯一表示符,只要内容不同唯一标识符就不同。启用 etag 之后,请求资源后的响应返回会增加一个 etag 字段,如下:
Etag: "FllOiaIvA1f-ftHGziLgMIMVkVw_"
- 当再次请求该资源时,请求头会带上
If-No-Match字段,值为之前响应的Etag的值。 - 服务端拿到请求头
If-Modified-Since的值和当前资源最后修改时间Last-Modified进行比对:
会有以下两种情况
If-No-Match != Etag:客户端的缓存不是最新更改的数据,返回新的资源,跟常规的HTTP请求响应的流程一样。If-No-Match = Etag:客户端的缓存的资源是最新的,无需再从服务端响应,此时返回304,告诉浏览器直接使用缓存。
Node服务器实现协商缓存
- 搭建服务器。
// service1.js
const http = require('http');
const fs = require('fs');
const crytpo = require("crypto");
http.createServer(function (req, res) {
const ctime = fs.statSync('./main.html').ctime.toGMTString();
const flag = crytpo.createHash("md5").update(data).digest("hex");
// 判断修改时间是否一致
const isSameCtime = req.headers['if-modified-since'] === ctime;
// 判断文件的MD5是否一致,不一致说明文件内容变更
const isSameFlag = req.headers['etag'] === flag;
// 两者满足其一则使用浏览器缓存
if (isSameCtime || isSameFlag) {
res.statusCode = 304;
res.end();
} else {
fs.readFile('./main.html', function (err, data) {
if (!err) {
// 获取文件的MD5
res.setHeader("Content-Type", "text/html");
res.setHeader("Last-Modified", ctime);
res.setHeader("Etag", flag);
res.write(data);
res.end();
}
})
}
}).listen(3001);
- 创建客户端文件
<!-- main.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>协商缓存</title>
</head>
<body>
<h1>协商缓存</h1>
</body>
</html>
启动服务node service1.js访问http://localhost:3001/打开控制台。
首先把“禁用缓存”勾选一下然后刷新页面,可以看到响应资源大小是464B,状态码为200,此时资源是从服务端直接获取并没有取缓存
取消勾选“禁用缓存”,把“保留日志”勾选上方便请求日志做对比。连续刷新几次可以看到状态码为304,响应资源为90B,和第一次的请求进行对比会发现资源是直接走的浏览器缓存
让我们再对比一下http头信息,进一步了解协商缓存
第一次请求及响应的头信息,因为第一次请求勾选了”禁用缓存“这一项,请求头中浏览器自动带上强缓存的标识
Pragma: no-cache表示禁用缓存。虽然响应头中带有Etag和Last-Modified这两个协商缓存的字段,但是浏览器并没有鸟这两个字段(强缓存:”老子是大爷“),依然是从服务器拉取最新的资源第二次请求及最后一次请求及响应头信息都是一致的,可以看到取消勾选”禁用缓存“这一项,请求头中会自动加上
If-Modified-Since: Fri, 14 Aug 2020 10:25:06 GMT和If-None-Match: d60b35d81aadf0a2b679a061ae681e3a这两个字段(这两个字段是因为第一次请求结束后浏览器根据响应头的信息中有Last-Modified和Etag缓存该字段)
用图说话
下面用一张图展示一下协商缓存的大致流程
Cache-Control在请求和响应头中使用的具体含义
| 指令 | 参数 | 请求报文中的作用 | 响应报文中的作用 |
|---|---|---|---|
| no-cache | 无 | 客户端提醒缓存服务器,在使用缓存前,不管缓存资源是否过期了,都必须进行校验 | 客户端提醒缓存服务器,在使用缓存前,不管缓存资源是否过期了,都必须进行校验 |
| max-age=[秒] | 秒 | 如果缓存资源的缓存时间值小于指定的时间值,则客户端接收缓存资源(如果值为0,缓存服务器通常需要将请求转发给源服务器进行响应,不使用缓存) | 在指定时间内,缓存服务器不再对资源的有效性进行确认,可以使用 |
| no-store | 无 | 暗示请求报文中可能含有机密信息,不可缓存 | 暗示响应报文中可能含有机密信息,不可缓存 |
| no-transform | 禁止代理改变实体主体的媒体类型(例如禁止代理压缩图片等) | 禁止代理改变实体主体的媒体类型(例如禁止代理压缩图片等) | |
| cache-extension | 新指令标记(token),如果缓存服务器不能理解,则忽略 | 新指令标记(token),如果缓存服务器不能理解,则忽略 | |
| max-stale(=[秒]) | 提示缓存服务器,即使资源过期也接收;或者过期后的指定时间内,客户端也会接收 | 无 | |
| min-fresh=[秒] | 提示缓存服务器,如果资源在指定时间内还没过期,则返回 | 无 | |
| only-if-cached | 如果缓存服务器有缓存该资源,则返回,不需要确认有效性。否则返回504网关超时 | 无 | |
| public | 无 | 明确指明其他用户也可以使用缓存资源 | |
| private | 无 | 缓存服务器只给指定的用户返回缓存资源,对于其他用户不返回缓存资源 | |
| must-revalidate | 无 | 缓存资源未过期,则返回,否则代理要向源服务器再次验证即将返回的响应缓存是否有效,如果连接不到源服务器,则返回504网关超时 | |
| proxy-revalidate | 无 | 所有缓存服务器在客户端请求返回响应之前,再次向源服务器验证缓存有效性 | |
| s-maxage=[秒] | 无 | 缓存资源的缓存时间小于指定时间,则可以返回缓存资源,只适用于公共缓存服务器 |
码字不易,点个赞鼓励一下呗