HTTP缓存
HTTP内容协商机制
不同浏览器对HTML标签的解析可能存在差异(例如,谷歌浏览器可能自动识别标签,而其他浏览器不会),这会导致页面渲染不一致。为了确保内容正确性,后端需通过响应头明确告知浏览器返回内容的类型和编码,这就是HTTP的内容协商机制。
代码示例与解析:
以下代码演示如何根据客户端的请求头有Accept返回JSON或HTML格式数据:
const server = http.createServer((req, res) => {
const { pathname } = url.parse(req.url);
if (pathname === '/') {
const accept = req.headers.accept;
if (accept.includes('application/json')) {
// 返回JSON数据
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(responseData));
} else {
// 默认返回HTML
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(toHTML(responseData));
}
}
});
- 通过
req.headers.accept获取客户端期望的响应类型。 - 设置
Content-Type响应头(如text/html; charset=utf-8),明确内容类型和编码,避免浏览器解析差异。
由此我们明白http存在这种内容协商机制
静态资源的传输
方法一:同步读取文件
if (fs.existsSync(filePath)) { // 检查文件是否存在
const ext = path.parse(filePath).ext; // 获取文件扩展名
if (ext === '') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
} else {
res.writeHead(200, { 'Content-Type': mime.getType(ext) });
}
const data = fs.readFileSync(filePath); // 同步读取文件
res.end(data); // 返回文件内容
}
-
缺点:
- 同步操作阻塞主线程,性能低。
- 一次性读取大文件可能导致内存溢出。
方法二:流式传输(推荐)
npm install mime@3
const mime = require('mime');
const stream = fs.createReadStream(filePath); // 创建可读流
res.writeHead(200, { 'Content-Type': mime.getType(ext) });
stream.pipe(res); // 通过管道流式传输到前端
-
优势:
-
非阻塞I/O,适合大文件传输。
-
内存占用低,按需加载数据块。
问题暴露: 即使使用流式传输,频繁刷新页面时仍需重复请求资源,加载速度未显著提升。
突破:(HTTP 缓存)将页面上长时间不更新的资源缓存到浏览器上,下次访问页面时该部分资源直接从缓存中获取,从而减少了网络请求的次数,提高了页面的加载速度。
突破性能瓶颈:HTTP缓存
HTTP强缓存
解决方案:
强缓存: 在响应头中设置 Cache-Control 字段, 该字段的值为 max-age=xxx , 表示缓存的有效期, 单位为秒。
代码示例:
const { ext } = path.parse(filePath);
res.writeHead(200, {
'Content-Type': mime.getType(ext),
'Cache-Control': 'max-age=86400', // 强缓存一天
'Last-Modified': stat.mtimeMs // 记录文件最后修改时间
});
const stream = fs.createReadStream(filePath);
stream.pipe(res);
-
执行逻辑:
- 首次请求时,浏览器缓存资源并记录
Last-Modified时间。 - 后续请求在缓存有效期内(86400秒)直接使用本地资源,无需请求服务器。
- 首次请求时,浏览器缓存资源并记录
http缓存也叫浏览器缓存,它是http响应头允许做的操作,又是资源缓存到了浏览器上。
size:memery cache命中缓存
问题:图片被缓存 index.html没有被缓存, 图片请求是在浏览器加载 html代码过程中发的ajax请求,html这个资源的请求是在浏览器加载url路径时发的。通过浏览器 url 地址栏请求的资源,就算后端响应头设置了Cache-Control,因为请求头中就会自动携带 Cache-Control: max-age=0 字段,后端声明的强缓存无效, 也就意味着这种资源无法被强缓存。
注意:不是说网页不会被强缓存,而是资源访问的方式导致这类资源无法命中强缓存。例如,通过<iframe>标签加载的资源可以被强缓存。至于通过浏览器地址栏直接请求的资源(如HTML),其无法被强缓存的原因在于:虽然这类资源会被浏览器缓存,但浏览器在刷新页面时,默认行为是跳过本地缓存,直接向服务器发起请求(地址栏请求默认携带Cache-Control: max-age=0)。因此,这类资源无法通过强缓存机制生效,但并不意味着它们完全不被缓存。
插曲:缓存的地方
被缓存的资源在浏览器的 cache Storage 中,本质上还是在本地硬盘上。
试验:将硬盘上的图片替换,查看浏览器页面图片是否被替换,修改图片资源,名字不变,结果没变。
原因:强缓存时效没过,访问的还是那个被缓存的名为xxx图片的资源
措施:Ctrl + F5(Windows)或 Cmd + Shift + R(Mac),强制刷新,替换图片出来,强制刷新导致浏览器所有的请求重新发送而不走缓存,强制刷新浏览器,会清空浏览器的 cache Storage。
问题:当公司修改了资源,又不能要求用户每次打开都强制刷新
解决:一般这种图片资源不会叫普通的名称,而是xxx+一段哈希值(随机值,只要图片更新了值会自动变化,代码会自动更新)。
协商缓存
问题:如何让浏览器地址栏访问的这份html资源缓存
突破点:req的 stats.mtimeMs(最后修改时间),这份资源虽不能强缓存但依旧被浏览器缓存下来,只是浏览器下一次刷新时依旧不会走缓存里面去取html而是问后端要。我们可以让它继续走协商缓存。
协商缓存: 强缓存对浏览器地址栏访问的资源无效,所以浏览器提供了协商缓存的机制。
1. Last-Modified / If-Modified-Since
const { ext } = path.parse(filePath)
const timeStamp = req.headers['if-modified-since']
let status = 200
if (timeStamp && Number(timeStamp) === stat.mtimeMs) {
status = 304 // 文件修改时间没变
}
res.writeHead(status, {
'Content-Type': mime.getType(ext),
'cache-control': 'max-age=86400', // 强缓存一天
'last-modified': stat.mtimeMs // 最后修改的时间
})
if (status === 200) {
const stream = fs.createReadStream(filePath) // 创建可读流
stream.pipe(res) // 将可读流的数据,通过管道,输出到前端
} else {
return res.end() // 文件最后一次修改时间没变,返回空
}
原理: 浏览器第一次访问资源时,响应头中携带 last-modified 字段,值为该资源的最后修改时间,当浏览器接收到响应头后,会在该资源再次被请求时,在请求头中自动携带 if-modified-since 字段,值为last-modified的值,后端检验请求头中的if-modified-since 的值和 last-modified 的值是否一致,如果一致,就返回 304 状态码,浏览器就会从缓存中获取该资源,如果不一致,就返回 200 状态码,浏览器就会重新请求该资源。
缺点:
Last-Modified 存在一些弊端:如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致返回相同的资源文件和 200。因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改了文件,那么服务端会认为资源还是命中了,不会返回正确的资源。
当文件被修改后但内容没有变更,last-modified 的值会更新,从而导致该重新重新被请求。
ETag / If-None-Match(推荐)
突破点:根据文章内容生成标识,以此判断是否要走缓存
npm i checksum(这个插件可以用来帮我们检查整个文件里的资源有没有被修改过),加密计算文件的MD5值 ,响应头里使用etag字段,值为加密后的值。
const checksum = require('checksum') // 计算文件的MD5值
const { ext } = path.parse(filePath)
const ifNoneMatch = req.headers['if-none-match']
let status = 200
if (ifNoneMatch === sum) { // 如果 ETag 头部和 sum 相同,返回 304
status = 304
}
checksum.file(filePath, (err, sum) => { // 识别文件资源,资源变更sum变更
// console.log(sum); // 6adb05c9d92089507689a7c2beecfc2d814b67eb
sum = `"${sum}"`
res.writeHead(200, {
'Content-Type': mime.getType(ext),
'cache-control': 'max-age=86400', // 强缓存一天
'etag': sum // eTag 头部,如果资源没有变,返回304
})
if (status === 200) {
const reaStream = fs.createReadStream(filePath)
reaStream.pipe(res) // 向前端返回文件
} else {
res.end()
}
})
第一次请求,返回的响应头
刷新后的请求头
强制刷新后
再刷新
ETag 是服务器响应请求时,返回当前资源的一个唯一标识(由服务器生成)只要资源有变化,Etag 就会重新生成。浏览器在下一次加载资源向服务器发起请求时,会将上一次返回的 ETag 值放到 request header 的 If-None-Match 中,服务器只需要对比客户端传来的 If-None-Match 跟自己服务器上该资源的 ETag 是否一致,就能很好的判断该资源相对于客户端而言是否被修改过了。如果服务器发现 ETag 匹配不上,那么直接以常规 GET/POST 200回报的形式将新的资源(当然也包括了新的 ETag)发送给客户端;如果 ETag 一致,则直接返回 304 知会客户端直接使用本地缓存即可。
- 优势: 基于文件内容生成唯一哈希(如MD5),精度极高。
- 缺点:不适合大文件,不适合图片资源。
强缓存与协商缓存的对比
| 机制 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 强缓存 | - 零网络请求,性能最优 | - 资源更新后用户可能看到旧版本 | 长期不变的静态资源(如图片) |
| 协商缓存 | - 精确判断资源是否变更 | - 需额外请求验证,增加延迟 | 频繁更新的动态资源(如HTML) |
只用强缓存可以把除url地址栏访问的资源缓存起来,但是资源更新了就无法第一时间让前端获取到,所以还需要协商缓存。 优先级规则:
- 强缓存优先:只要未过期,直接使用本地缓存,不触发协商缓存。
- 过期后协商:强缓存失效后,浏览器携带
If-Modified-Since或If-None-Match向服务器验证资源是否变更。
代码示例:
// 强缓存+协商缓存组合
res.writeHead(200, {
'Cache-Control': 'max-age=86400', // 强缓存一天
'ETag': fileHash, // 内容哈希标识
'Last-Modified': stat.mtimeMs // 最后修改时间
});
-
执行逻辑:
- 首次请求:返回资源并设置缓存头。
- 再次请求:若未过期(
max-age有效),直接使用强缓存。 - 过期后:携带
If-None-Match(ETag值)请求服务器,若未变更,返回304,否则返回新资源。
问题:只要命中了强缓存,就不会走协商缓存, 只有强缓存到期,才会走协商缓存。
办法:为了保证文件资源更新前端能够及时获取到, 一般会在文件名后面加上文件指纹(用内容生成 hash 值), 这样文件指纹就会改变, 从而保证文件资源的更新。
五、总结(强缓存+协商缓存+文件指纹)
强缓存+协商缓存
- 执行顺序:
强缓存 → 过期后触发协商缓存 → 验证ETag或Last-Modified。
文件指纹
- 文件名添加哈希值。
- 内容变化时哈希值自动更新,确保浏览器获取最新资源。