手把手教你如何实现服务器强缓存&协商缓存(nodejs实战篇)

2,888 阅读6分钟

代码传送门

概念

一、强缓存

在向服务端发送请求之前,浏览器会根据请求头部携带的Cache-Control或者Expires来尝试命中强缓存,如果命中则直接返回资源,不会再向服务器发送请求

强缓存的原理是为资源文件设置一个时间,只要没超过这个时间,都使用缓存,不管服务器文件是否更新。

  • Expires:HTTP/1.0 表示资源过期时间,缺点是采用的是本机时间(绝对时间)
  • Cache-Control:HTTP/1.1 为了解决Expires时间不准的问题(相对时间)

注意⚠️:
Expires 和 Cache-Control 的时间都是取本机时间,所以都可以在本地篡改时间使缓存失效,Expires最大的问题是本地时间和服务器时间不一致导致的缓存时间和预期不一致。Cache-Control采取相对时间,至少保证缓存时间长度是正确的。

简单来说,在本机时间没有被篡改的情况下,Expires的缓存时间都有很大可能和预期不一致!而Cache-Control采用相对时间,可以保证缓存时间的正确性。

Cache-Control

  • Cache-Control: max-age=300 表示服务器再次获取该资源时没有超过300s 则命中缓存
  • Cache-Control: no-cache 表示当前资源跳过强缓存,可以使用协商缓存(如果有)
  • Cache-Control: no-store 不使用任何缓存(包括强缓存和协商缓存),强制服务器返回资源
  • Cache-Control: private 代理服务器不能缓存资源,只有客户端本地可以缓存
  • Cache-Control: public 大家都可以缓存这个资源

二、协商缓存

如果服务器携带如下响应头,则会进行协商缓存,协商缓存需要发送请求,其原理就是客户端会发送请求询问服务器本地的文件是否过期,如果服务端说没过期,你可以使用本地缓存,则客户端会使用缓存,否则服务端会重新发送资源文件给客户端。

响应头

  • ETag 根据文件内容生成的代码
  • Last-Modified 服务器的文件最后修改时间

请求头

  • If-None-Match 对应ETag,第一次请求后,客户端会储存 ETag 并在下一次请求时赋给If-None-Match
  • If-Modified-Since 对应Last-Modified,第一次请求后,客户端会储存 Last-Modified 并在下一次请求时赋给If-Modified-Since

实战

1.目录结构
```
 |-- http-cache
 |---- static     静态资源文件夹
 |-------- index.html  html
 |-------- queue.png   测试图片
 |---- server.js  服务器入口文件
 |---- utils.js   文件读取之类的工具类
```

image.png

2.直接使用http来启动一个服务
 // server.js
 const http = require("http");
 ​
 const server = http.createServer((req, res) => {
   // 测试一下
   res.statusCode=200;
   res.end("Hello World");
 });
 ​
 server.listen(3333, () => {
   console.log("server listening on 3333");
 });

服务器的启动很简单就不赘述了,服务启动后在浏览器访问localhost:3333就可以看到效果

image.png

3. html

非常简单,展示 Hello World 以及一张图片。

 <html lang="en">
   <head>
     <title>Document</title>
   </head>
   <body>
     <div>Hello World</div>
     <img src="./queue.png" width="200" />
   </body>
 </html>
4. 工具类

对服务器来说,读取文件的操作是必不可少的,协商缓存还需要获取文件的修改时间,我们简单写几个工具函数

工具函数写的很粗糙,但是对测试来说够用了。

 // utils.js
 const fs = require("fs");
 const path = require("path");
 ​
 // 读取文件
 exports.readFileSync = function readFileSync(filePath, options) {
   const absPath = path.resolve(__dirname, "./static", normalizePath(filePath));
 ​
   try {
     fs.accessSync(absPath, fs.constants.W_OK);
 ​
     return fs.readFileSync(absPath, options);
   } catch (err) {
     console.log(`${absPath} 文件访问受限!`, err);
     return;
   }
 };
 ​
 // 使用 fs.statSync 获取文件的stat,stat包含了一系列文件修改时间、创建时间等统计信息
 exports.getFileStat = function getFileStat(filePath) {
   const absPath = path.resolve(__dirname, "./static", normalizePath(filePath));
 ​
   try {
     fs.accessSync(absPath, fs.constants.W_OK);
 ​
     return fs.statSync(absPath);
   } catch (err) {
     console.log(`${absPath} 文件访问受限!`, err);
     return;
   }
 };
 ​
 // 处理绝对路径
 function normalizePath(path) {
   return path.replace(/^\/+/, "");
 }
 ​
5. 服务器代码逻辑

我们来使用两个路由分别模拟强缓存协商缓存

  • /cache1 强缓存
  • /cache2 协商缓存

修改 server.js,为了方便起见,我直接在请求开始就读取了 index.html文件内容,免得后面判断里要写重复内容

 // server.js
 const DEFAULT_PATH = "index.html";
 ​
 const server = http.createServer((req, res) => {
   // 图方便,在这直接读取了 index.html 文件内容
   const data = readFileSync(DEFAULT_PATH, { encoding: "utf-8" });
   res.setHeader("content-type", "text/html; charset=utf-8");
 ​
   console.log("请求资源:", req.url);
 ​
   // 路由 cache1 做强缓存处理
   if (req.url === "/cache1") {
 ​
   }
 ​
   // 路由 cache2 做协商缓存处理
   else if (req.url === "/cache2") {
 ​
   }
 ​
   // 其他资源,图片等 使用强缓存
   else {
 ​
   }
 });
  1. 强缓存逻辑

Cache-Control 为例,对index.html做强缓存处理

 // 路由 cache1 做强缓存处理
 if (req.url === "/cache1") {
   res.setHeader("Cache-Control", "max-age=120");
   res.end(data);
 }

对图片资源做缓存处理

 // 其他资源,图片等 使用强缓存
 else {
   res.setHeader("cache-control", "max-age=60");
   res.setHeader("content-type", "image/png");
   res.end(readFileSync(req.url));
 }
  1. 测试 - index.html强缓存失效的原因!

代码很完美,打开浏览器输入 localhost:3333/cache1 来试试看!

结果不对呀,图片确实被缓存了,配置的Content-Type生效了,但是 index.html 并没有被缓存,明明是相同的配置,为什么两种资源一个有缓存一个没有缓存?

image.png

点进详情看一看,浏览器为index.html资源文件配置了Cache-Control: max-age=0,但是我并没有在浏览器开启停用缓存的配置。

我做了几次实验发现,浏览器在加载url栏输入地址的根资源时,会默认配置Cache-Control: max-age=0避免对根资源文件使用强缓存,当你的url栏输入了 http://localhost:3333/cache1时,这个 cache1 就是根资源文件。

你也可以试试直接输入 http://localhost:3333/queue.png,会导致图片缓存失效。

image.png

  1. 协商缓存逻辑

使用Last-Modified配置为例,在访问资源的时候,客户端发起的请求是由浏览器代理的,所以我们不需要设置客户端请求的头部,都交给浏览器来就行了。

如果要开启协商缓存,服务器只需要在响应头加上 Last-Modified 即可,浏览器收到之后会自动配置请求头if-modified-since,要注意客户端第一次请求的时候,请求头是不会if-modified-since的,只有第二次之后会有,看代码实现:

//路由 cache2 做协商缓存处理
else if (req.url === "/cache2") {
  // 配置 "Cache-Control: no-cache" 关闭强缓存,需要浏览器发送请求来进行协商缓存
  res.setHeader("Cache-Control", "no-cache");
  
  // 拿到客户端的 "if-modified-since" 头部配置
  const ifModifiedSince = req.headers["if-modified-since"];
  const stat = getFileStat(DEFAULT_PATH);
  const mtimeStr = stat.mtime.toISOString();
  console.log("文件修改时间 ", stat.mtime);

  if (ifModifiedSince) {
    console.log("客户端传入时间 ", ifModifiedSince);
    res.setHeader("Last-Modified", mtimeStr);

    // 资源失效了 返回新的资源,状态码 200,重新返回新的资源文件给浏览器
    if (mtimeStr > ifModifiedSince) {
      res.statusCode = 200;
      res.end(data);
    } else {
      // 资源没失效,浏览器可以使用本地资源,状态码 304,返回空资源就可以了
      res.statusCode = 304;
      res.end();
    }
  } else {
		// 第一次访问该资源
    res.setHeader("Last-Modified", mtimeStr);
    res.end(data);
  }
}
  1. 测试协商缓存是否生效

访问 localhost:3333/cache2,刷新一次,就能看到cache2资源返回状态码是304

image.png

看这段代码,状态304时,服务器res.end()返回了空资源,浏览器却正确的获取到了资源,说明协商缓存生效!

 // 资源没失效,浏览器可以使用本地资源,状态码 304,返回空资源就可以了
 res.statusCode = 304;
 res.end();

代码贴在文章顶部了,代码本身也不复杂,感兴趣的可以拷贝下来自己添加响应头来做其他测试。 感谢阅读!