用Node实现HTTP缓存(强制、协商),附原理

237 阅读4分钟

服务端缓存

缓存简介

  • 贴个知乎大佬的图:
    • v2-71a15fbb93c2ad276e88c71cfe98d6ac_720w.webp

后端的缓存一般由强制缓存和协商缓存组成

  • 若有缓存响应信息(响应头),则浏览器会将响应结果缓存在本地(浏览器内存/硬盘)。
  • 下一次浏览器发送请求时判断请求信息。
    • 符合强制缓存的请求直接获取浏览器缓存文件且返回状态码200
    • 存在协商缓存的请求则访问服务器,若符合条件则通知浏览器获取浏览器的缓存且返回304
  • 补充:默认不缓存首页
  • 缓存信息补充:2022-06-05-19-57-40.png

缓存配置

// 每次请求都询问服务器,协商缓存
res.setHeader('Cache-Control','no-cache');
// 不走缓存,每次都获取最新的资源
res.setHeader('Cache-Control','no-store');

强制缓存

  • 强制缓存配置
    • 时间配置:设置时间节点,若请求时机未超过则走缓存,反之则走请求。
      • Expires(老版本配置)
      • Cache-Control(新版本配置)
res.setHeader('Expires',new Date().toGMTString());//老版本配置
res.setHeader('Cache-Control','max-age=10');//新版本配置

协商缓存

  • 开启协商缓存:res.setHeader('Cache-Control','no-cache');

协商时间 -- 根据文件修改时间协商缓存

  • 服务端设置
    1. 服务端设置响应头:res.setHeader('Cache-Control','no-cache');
    2. 服务端设置协商规则:res.setHeader('Last-Modified',time)
    3. 服务端判断条件是否符合返回304:res.statusCode = 304;
  • 浏览器根据响应头自动携带请求时间:req.header['if-modified-since']
  • 缺点:不够精确,若时间到达且资源未改变,会重新获取资源。
// 缓存一天
let time = Date.now().getTime() + 24 * 60 * 60 * 1000;
// 第一次请求,返回协商时间
res.setHeader('Last-Modified',time);
// 若服务端设置了Last-Modified,则浏览器以后发送该请求时,会携带当前请求的时间。
let LastModify = req.header['if-modified-since'];
LastModify = new Date(LastModify).getTime()
if(time >= LastModify){
    // 命中缓存,浏览器自行获取其缓存
    res.statusCode = 304;
    res.end();
}

协商内容:根据文件内容协商缓存

  • 前置知识点:md5:摘要算法:将内容转换为另一段内容,非加密算法。
    • 特点:
      • 不可逆:转换后的内容无法逆推回原内容。
      • 雪崩效应:相似的内容转换后的无迹可寻,但相同的内容转换后是相同的。
      • 内容等长:转换后的内容长度一致。
  • 服务端设置
    1. 服务端设置响应头:res.setHeader('Cache-Control','no-cache');
    2. 服务端设置协商规则:res.setHeader('Etag',contentHash)
    3. 服务端判断条件是否符合返回304:res.statusCode = 304;
  • 浏览器根据响应头自动携带哈希值req.header['if-none-match','静态资源哈希值]
  • 特点:

比较方式比较精准,但是若静态资源较大,每次生成哈希值非常耗费性能。 所以一般情况下会折中:采用文件资源的一部分(如:开头几行)加上文件大小生成哈希值判断文件是否被修改过。

// 内置若干摘要算法
const crypto = require('crypto'); 
// 获取文件
const fileHash = fs.readFileSync(filePath)
// md5算法 可支持buffer内容转换,将内容摘要为base64编码
const contentHash = crypto.createHash('md5').update(fileHash).digest('base64'); 
// 返回该文件的内容摘要标识
res.setHeader('Etag',contentHash);
// 获取浏览器缓存中的摘要标识对比
let ifNoneMatch = req.headers['if-none-match']; 
if(ifNoneMatch === contentHash){
    res.statusCode = 304
    res.end()
}else{
    // 根据新的资源重新生成哈希值并返回新的资源
}

总结

强制缓存

  • 服务端返回设置的时间值:res.setHeader('Expires',time = new Date().toGMTString())
  • 浏览器每次请求该资源,判断请求时机是否小于time,是则走浏览器缓存,反之则请求服务器。

协商缓存:不管是否走缓存,都会经过服务器判断是否协商成功。

  • 服务端设置协商响应头:res.setHeader('Cache-Control','no-cache');
  • 时间协商:服务端每次收到资源访问,都会对比时间。若比约定时间小,则走缓存(304),反之则返回新资源(200)
    • 服务端返回响应头:res.setHeader('Last-Modified',time)
    • 则浏览器根据响应头自动携带请求时间req.header['if-modified-since']
  • 哈希值协商:服务端每次收到资源访问,都会重新生成哈希值,若相同,则走缓存(304),反之则返回新资源(200)
    • 服务端返回响应头:res.setHeader('Etag',contentHash)
    • 浏览器根据响应头自动携带哈希值req.header['if-none-match',contentHash]

开发缓存策略(强制缓存 + 协商缓存)

  1. 先走强制缓存:设置过期时间
  2. 若强制缓存不命中,则协商缓存:根据业务选择时间或者内容协商,也可共用。
    1. 若协商缓存命中,则重置过期时间:expires
    2. 若协商缓存不命中,则重新获取资源,且重置过期时间:expires