缓存分为强制缓存和协商缓存。
强制缓存和协商缓存最大的区别:命中缓存后的情况
- 强制缓存命中缓存,直接使用缓存的资源,不再向服务器发送请求
协商缓存命中缓存,但是还会继续向服务器发送请求,之后根据服务器返回的数据,对缓存资源进行判断使用哪个。
强缓存
Expires:http1.0版本适用
Cache-Control:http1.1版本适用,更多使用
Expires
Expires值为服务端返回的到期时间。下次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。【http1.0版本适用,现在很少使用】。
还有另一个问题,到期时间是服务端生成,和客户端时间有误差,会导致命中的缓存有误差,后边就使用Cache-Control替换。Cache-Control
Cache-Control是目前最重要的缓存策略,常见取值有private,public,no-cache,max-age,no-store。
默认为private。private:客户端可以缓存
- public:客户端和代理服务器都可以缓存
- max-age=[number]:缓存的内容在number秒后失效
- no-cache:不使用本地缓存,需要使用协商缓存来验证缓存数据
- no-store:所有内容都不缓存,强制缓存和协商缓存都不触发。
初次访问图片资源,未添加缓存设置。请求时间6ms
添加强制缓存设置。请求的图片资源直接从缓存读取,时间为0ms。
res.writeHead(200, {
"Content-Type": "images/png",
"Cache-Control": "max-age=31536000",
});
data = await readFile("avator.png");
协商缓存
协商缓存需要进行比较判断是否可以使用缓存。
浏览器第一次请求数据时,服务器会将缓存标识和数据一起返回给客户端,客户端将缓存标识和数据备份至缓存数据库中。再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功可以使用缓存后,返回状态码304,通知客户端可以使用缓存数据。
协商缓存主要有四个头字段,两两组合配合使用
- Last-Modified和If-Modified-Since 一组
- Etag和If-None-Match 一组
If-Modified-Since 和 Last-Modified
客户端第一次发送请求时,服务端会返回一个Last-Modified响应头,该字段是一个标准时间。客户端再次请求服务器时携带上If-Modified-Since请求头字段【值就是服务器返回的Last-Modified的值】。
服务器收到请求后进行比较判断,如果一样则说明可以使用缓存,返回状态码304。不一样的话就会返回新文件给客户端并更新Last- Modified响应头字段的值。使用该策略的优点:
使用缓存,大大节省流量和带宽,减轻服务器的压力。使用该策略的缺点:
Last-Modified过期时间只能精确到秒。如果在同一秒内同时修改文件和获取文件,客户端就获取不到最新的文件。
Etag和If-None-Match【优先级高于last-modified】
为了解决缓存时间只能精确到秒的问题,引入了Etag响应头。
Etag是由文件修改时间与文件大小计算生成。只有当文件内容修改才会引起Etag值的变化。
当客户端第一次请求服务器,服务端返回Etag响应头。客户端再次request请求服务器的时候会带上If-None-Match请求头字段【该字段的值是服务器返回的Etag值】。
服务器接收到请求后回比较两个值是否一样,一样就通知客户端使用缓存,返回状态码304。不一样则返回新文件并更新Etag响应头的字段值。
使用Etag策略的优点
- 节省流量带宽,减少服务器压力。
- 解决了一秒内同时修改并读取文件的问题。
完整项目代码
const fs = require("fs");
const path = require("path");
const readFile = (...rest) => {
let url = path.join(__dirname, "static", ...rest);
return new Promise((resolve, reject) => {
fs.readFile(url, (err, data) => {
if (err) reject(err);
resolve(data);
});
});
};
module.exports = readFile;
const http = require("http");
const readFile = require("./readFile");
let date = new Date().toUTCString();
// etag的生成规则有服务器决定,做一个服务端的资源标识。当资源内容发生改变,etag就发生改变
// 比如md5算法
let etag = "thisisetag;";
const server = http.createServer(async function (req, res) {
let url = req.url;
console.log(`${url}`);
let data = Object.create(null);
// console.log(`${req.method} ${req.headers.host} ${url}`);
if (url === "/favicon.ico") {
return res.end();
}
switch (url) {
case "/":
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
data = await readFile("index.html");
break;
case "/avator.png":
res.writeHead(200, {
"Content-Type": "images/png",
"Cache-Control": "max-age=31536000",
});
data = await readFile("avator.png");
break;
case "/bootstrap.css":
// last-Modified / if-modified-since:对比的时间
if (req.headers["if-modified-since"] === date) {
// 说明对比成功,可以实现缓存
res.writeHead(304, {
"Content-Type": "text/css; charset=utf-8",
"Cache-Control": "no-cache", //使用协商缓存
"Last-Modified": date,
});
} else {
res.writeHead(200, {
"Content-Type": "text/css; charset=utf-8",
"Cache-Control": "no-cache", //使用协商缓存
"Last-Modified": date,
});
}
data = await readFile("bootstrap.css");
break;
case "/index.js":
//使用对比缓存,对比内容
if (req.headers["if-none-match"] === etag) {
res.writeHead(304, {
"Content-Type": "text/javascript; charset=utf-8",
"Cache-Control": "no-cache",
etag: etag,
});
} else {
res.writeHead(200, {
"Content-Type": "text/javascript; charset=utf-8",
"Cache-Control": "no-cache",
etag: etag,
});
}
data = await readFile("index.js");
break;
default:
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
data = url + " 404 访问的页面不存在";
break;
}
res.end(data);
});
server.listen(8123, () => {
console.log("server listening on 8123");
});
在static目录下创建index.html
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./bootstrap.css" />
</head>
<body>
<h1>index</h1>
<img src="./avator.png">
</body>
- 添加一个avator.png图片
- 引入bootstrap.css
- 创建index.js文件
static目录下共有4个文件。