用Node实现HTTP缓存(强制、协商),附原理
服务端缓存
缓存简介
- 贴个知乎大佬的图:
后端的缓存一般由强制缓存和协商缓存组成
- 若有缓存响应信息(响应头),则浏览器会将响应结果缓存在本地(浏览器内存/硬盘)。
- 下一次浏览器发送请求时判断请求信息。
- 符合强制缓存的请求直接获取浏览器缓存文件且返回状态码200。
- 存在协商缓存的请求则访问服务器,若符合条件则通知浏览器获取浏览器的缓存且返回304。
- 补充:默认不缓存首页
- 缓存信息补充:
缓存配置
// 每次请求都询问服务器,协商缓存
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');
协商时间 -- 根据文件修改时间协商缓存
- 服务端设置
- 服务端设置响应头:
res.setHeader('Cache-Control','no-cache');
- 服务端设置协商规则:
res.setHeader('Last-Modified',time)
- 服务端判断条件是否符合返回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:摘要算法:将内容转换为另一段内容,非加密算法。
- 特点:
- 不可逆:转换后的内容无法逆推回原内容。
- 雪崩效应:相似的内容转换后的无迹可寻,但相同的内容转换后是相同的。
- 内容等长:转换后的内容长度一致。
- 特点:
- 服务端设置
- 服务端设置响应头:
res.setHeader('Cache-Control','no-cache')
; - 服务端设置协商规则:
res.setHeader('Etag',contentHash)
- 服务端判断条件是否符合返回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]
。
- 服务端返回响应头:
开发缓存策略(强制缓存 + 协商缓存)
- 先走强制缓存:设置过期时间
- 若强制缓存不命中,则协商缓存:根据业务选择时间或者内容协商,也可共用。
- 若协商缓存命中,则重置过期时间:
expires
- 若协商缓存不命中,则重新获取资源,且重置过期时间:
expires
- 若协商缓存命中,则重置过期时间:
转载自:https://juejin.cn/post/7239897482103668795