缓存分为强制缓存和协商缓存。
强制缓存和协商缓存最大的区别:命中缓存后的情况

  • 强制缓存命中缓存,直接使用缓存的资源,不再向服务器发送请求
  • 协商缓存命中缓存,但是还会继续向服务器发送请求,之后根据服务器返回的数据,对缓存资源进行判断使用哪个。

    强缓存

  • 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
image.png
添加强制缓存设置。请求的图片资源直接从缓存读取,时间为0ms。

  1. res.writeHead(200, {
  2. "Content-Type": "images/png",
  3. "Cache-Control": "max-age=31536000",
  4. });
  5. data = await readFile("avator.png");

image.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过期时间只能精确到秒。如果在同一秒内同时修改文件和获取文件,客户端就获取不到最新的文件。
    image.png

Etag和If-None-Match【优先级高于last-modified】

为了解决缓存时间只能精确到秒的问题,引入了Etag响应头。
Etag是由文件修改时间与文件大小计算生成。只有当文件内容修改才会引起Etag值的变化。
当客户端第一次请求服务器,服务端返回Etag响应头。客户端再次request请求服务器的时候会带上If-None-Match请求头字段【该字段的值是服务器返回的Etag值】。
服务器接收到请求后回比较两个值是否一样,一样就通知客户端使用缓存,返回状态码304。不一样则返回新文件并更新Etag响应头的字段值。

使用Etag策略的优点

  • 节省流量带宽,减少服务器压力。
  • 解决了一秒内同时修改并读取文件的问题。

image.png

完整项目代码

  1. const fs = require("fs");
  2. const path = require("path");
  3. const readFile = (...rest) => {
  4. let url = path.join(__dirname, "static", ...rest);
  5. return new Promise((resolve, reject) => {
  6. fs.readFile(url, (err, data) => {
  7. if (err) reject(err);
  8. resolve(data);
  9. });
  10. });
  11. };
  12. module.exports = readFile;
  1. const http = require("http");
  2. const readFile = require("./readFile");
  3. let date = new Date().toUTCString();
  4. // etag的生成规则有服务器决定,做一个服务端的资源标识。当资源内容发生改变,etag就发生改变
  5. // 比如md5算法
  6. let etag = "thisisetag;";
  7. const server = http.createServer(async function (req, res) {
  8. let url = req.url;
  9. console.log(`${url}`);
  10. let data = Object.create(null);
  11. // console.log(`${req.method} ${req.headers.host} ${url}`);
  12. if (url === "/favicon.ico") {
  13. return res.end();
  14. }
  15. switch (url) {
  16. case "/":
  17. res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
  18. data = await readFile("index.html");
  19. break;
  20. case "/avator.png":
  21. res.writeHead(200, {
  22. "Content-Type": "images/png",
  23. "Cache-Control": "max-age=31536000",
  24. });
  25. data = await readFile("avator.png");
  26. break;
  27. case "/bootstrap.css":
  28. // last-Modified / if-modified-since:对比的时间
  29. if (req.headers["if-modified-since"] === date) {
  30. // 说明对比成功,可以实现缓存
  31. res.writeHead(304, {
  32. "Content-Type": "text/css; charset=utf-8",
  33. "Cache-Control": "no-cache", //使用协商缓存
  34. "Last-Modified": date,
  35. });
  36. } else {
  37. res.writeHead(200, {
  38. "Content-Type": "text/css; charset=utf-8",
  39. "Cache-Control": "no-cache", //使用协商缓存
  40. "Last-Modified": date,
  41. });
  42. }
  43. data = await readFile("bootstrap.css");
  44. break;
  45. case "/index.js":
  46. //使用对比缓存,对比内容
  47. if (req.headers["if-none-match"] === etag) {
  48. res.writeHead(304, {
  49. "Content-Type": "text/javascript; charset=utf-8",
  50. "Cache-Control": "no-cache",
  51. etag: etag,
  52. });
  53. } else {
  54. res.writeHead(200, {
  55. "Content-Type": "text/javascript; charset=utf-8",
  56. "Cache-Control": "no-cache",
  57. etag: etag,
  58. });
  59. }
  60. data = await readFile("index.js");
  61. break;
  62. default:
  63. res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
  64. data = url + " 404 访问的页面不存在";
  65. break;
  66. }
  67. res.end(data);
  68. });
  69. server.listen(8123, () => {
  70. console.log("server listening on 8123");
  71. });

在static目录下创建index.html

  1. <head>
  2. <meta charset="UTF-8">
  3. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  4. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  5. <title>Document</title>
  6. <link rel="stylesheet" href="./bootstrap.css" />
  7. </head>
  8. <body>
  9. <h1>index</h1>
  10. <img src="./avator.png">
  11. </body>
  • 添加一个avator.png图片
  • 引入bootstrap.css
  • 创建index.js文件

static目录下共有4个文件。