缓存是一种保存资源副本并在下次请求时直接使用该副本的技术,通过复用以前获取的资源,可以显着提高网站性能,降低服务器处理压力,减少了等待时间和网络流量。通过使用 HTTP缓存,变得更加响应性。

这篇文章会介绍三种缓存机制在nodejs中的实现,分别是:

  • 强制缓存 Cache-Control/Expires
  • 对比缓存

    • Last-Modified/If-Modified-Since
    • Etag/If-None-Match

两类缓存规则的不同

  • 强制缓存如果生效,不需要再和服务器发生交互,对比缓存不管是否生效,都需要与服务端发生交互。
  • 强制缓存优先级高于对比缓存强制缓存生效时,不再执行对比缓存规则。

新建http服务

为了方便测试,新建一个简单的http服务进行调试:

const http = require("http")
const url = require("url")
const mime = require("mime")
const fs = require("fs")

const server = http.createServer((req, res) => {
  const { pathname } = url.parse(req.url, true)
  const abspath = process.cwd() + pathname
  fs.stat(abspath, handleRequest)

  // 判断是文件还是文件夹
  function handleRequest(err, statObj) {
    if (err || statObj.isDirectory()) return sendError(err)
    sendFile()
  }
  // 响应错误请求
  function sendError(error) {
    res.statusCode = 404
    res.end(`Not Found \r\n ${error.toString()}`)
  }
  // 响应文件请求
  function sendFile() {
    res.setHeader("Content-Type", mime.getType(abspath) + ";charset=utf-8")
    fs.createReadStream(abspath).pipe(res)
  }
})

server.listen(3000, () => console.log("serving http://127.0.0.1:3000"))

强制缓存

强制缓存指的是在缓存数据未失效的情况下,可以直接使用缓存数据,浏览器通过服务器响应的header获取缓存规则信息。对于强制缓存,响应头header使用Cache-Control/Expires来标明失效规则。

Expires

Expires是HTTP1.0的东西,现在默认浏览器均默认使用HTTP 1.1,所以它的作用基本忽略,我们在响应头header设置Expires,浏览器根据它的到期时间来决定是否使用缓存数据:

res.setHeader("Expries",new Date(Date.now()+5*1000).toUTCString());

Cache-Control

Cache-Control是最重要的规则。常见的取值有private、public、no-cache、max-age,no-store,默认为private。

  • private 客户端可以缓存
  • public 可以被任何中间人(比如中间代理、CDN等)缓存
  • max-age=xxx 缓存的内容将在 xxx 秒后失效(单位是秒)
  • no-cache 需要使用对比缓存来验证缓存数据
  • no-store 所有内容都不会缓存,强制缓存对比缓存都不会触发

可以在handleRequest方法中添加给响应头设置Cache-Control,在浏览器刷新查看效果:

function handleRequest(err, statObj) {
    ...
    res.setHeader("Cache-Control","max-age=10");
    sendFile()
}

如果经常调试前端项目的开发人员,经常会把控制台Disable cache给勾上,这里记得一定要关掉它:

image.png

不出意外的话,可以在Network的请求中看到信息:

Status Code: 200 OK (from disk cache)

对比缓存

对比缓存,服务器将文件的修改信息发送给客户端(浏览器),客户端在下次请求的时候顺便带上,然后服务端就可以拿到上一次的修改信息,跟本地的文件修改信息做比较,告诉客户端是否用缓存数据还是用最新数据了

Last-Modified/If-Modified-Since 对比时间

通过statObjctime属性可以获取文件的修改时间,将这个修改信息通过请求头Last-Modified属性发送给浏览器:

const serverTime = statObj.ctime.toUTCString()
res.setHeader("Last-Modified", serverTime)

下次客户端请求的时候,也会在请求头通过if-modified-since带上:

const clientTime = req.headers["if-modified-since"]

修改handleRequest方法如下:

function handleRequest(err, statObj) {
    if (err || statObj.isDirectory()) return sendError(err)
    const clientTime = req.headers["if-modified-since"]
    const serverTime = statObj.ctime.toUTCString()
    // 如果本地的文件修改时间和浏览器返回的修改时间相同,则使用缓存的数据,返回304
    if (clientTime === serverTime) {
        res.statusCode = 304
        return res.end()
    }
    res.setHeader("Last-Modified", serverTime)
    res.setHeader("Cache-Control", "no-cache") //  对比缓存验证缓存数据
    sendFile()
  }

不过这种方式有两个弊端:

  • 如果一个文件被误修改,然后修改被撤销,这样内容虽然没变化,但最新修改时间会变
  • 文件被周期性的修改,文件内容没有变化,但最新修改时间会变化

Etag/If-None-Match 对比内容

上面提到的两个弊端,可以通过Etag/If-None-Match方式解决,也就是内容对比,不过Etag生成有一定的开销,如果文件频繁变化对服务器有额外压力。

当然我们不可能将内容都存在header里面,这里可以通过crypto将文件内容加密成一串秘钥,写在headerEtag属性中:

const crypto = require("crypto");
...
const md5 = crypto.createHash("md5"); // md5加密
const rs = fs.createReadStream(abspath); // 创建可读流
const data = [];
rs.on("data",(buffer)=>{
    md5.update(buffer); // 读取文件内容过程中加密
    data.push(buffer);
});
rs.on("end",()=>{
    const serverMatch = md5.digest("base64"); // 加密后的文件
    const clientMatch = req.headers["if-none-match"]; // 客户端下次请求会带上serverMatch
    if(clientMatch === serverMatch){ // 对比文件内容是否相同
        res.statusCode = 304;
        return res.end(null);
    }
    // 设置 ETag
    res.setHeader("ETag", serverMatch)
    res.end(Buffer.concat(data));
})

整合

我们可以在业务中根据自己的需要,设置对应的缓存方式,这里通过写个通用方法,将这三种模式整合起来:

  function cache(statObj) {
    // 强制缓存
    res.setHeader("Cache-Control", "max-age=60")

    // 时间对比
    let lastModified = statObj.ctime.toUTCString()
    let modifiedSince = req.headers["if-modified-since"]
    res.setHeader("Last-Modified", lastModified)
    if (modifiedSince !== lastModified) return false

    // 内容对比
    let etag = statObj.size + ""
    let noneMatch = req.headers["if-none-match"]
    res.setHeader("ETag", etag)
    if (etag !== noneMatch) return false

    return true
  }
  ...
  if(cache(statObj)){
    res.statusCode = 304
    return res.end(null)
  }

参考文章:
彻底弄懂HTTP缓存机制及原理
HTTP 缓存
协商缓存


chenwl
117 声望5 粉丝

平坦的路面上曲折前行