什么是强缓存?
强缓存:直接拿走不用问
-
场景:朋友说:"这本书你可以随便看 1 个月,期间不用问我"
-
过程:
-
浏览器:查本地缓存(书架)
-
发现资源在有效期内(书没到1个月)
-
直接使用缓存(看书),不联系服务器(不打扰朋友)
-
-
特征:
- 速度快(不用沟通)
简单来说我向朋友借了本书,他规定了借阅时间,在此期间,如果我想要看这本书,就可以到书架(浏览器)上拿,不需要和朋友说。
浏览器第一次向服务端发送请求,拿到一张图片,服务端说明了可以让浏览器进行缓存(Cache storage),并规定了缓存时间,那么在这段时间内,浏览器不需要再次向服务端请求这张图片,而是直接取缓存取就好了。
如何实现?
以下是一个简单的 Node.js 静态文件服务器,监听 3000 端口。浏览器访问 http://localhost:3000 或带 URL(如 /index.html),服务端从 www 目录查找对应文件。如果请求的是目录,自动返回目录下的 index.html。文件存在时,返回 200 状态码、设置 MIME 类型和 1 天缓存,并通过流传输文件内容;文件不存在时,返回 404 错误页面。前后端未分离,前端内容由静态文件直接在浏览器渲染。
// 引入 Node.js 内置的 http 模块,用于创建 HTTP 服务器
const http = require('http')
// 引入 Node.js 内置的 path 模块,用于处理文件路径
const path = require('path')
// 引入 Node.js 内置的 fs 模块,用于文件操作(如读取、检查文件是否存在)
const fs = require('fs')
// 引入 mime 第三方模块,用于根据文件扩展名获取对应的 Content-Type
const mime = require('mime')
// 创建 HTTP 服务器,定义请求处理函数,req 是请求对象,res 是响应对象
const server = http.createServer((req, res) => {
// 根据请求的 URL,生成服务器上文件的绝对路径,www 是静态文件目录
let filePath = path.resolve(__dirname, path.join('www', req.url))
// 同步检查请求的文件或目录是否存在
if (fs.existsSync(filePath)) {
// 获取文件的详细信息(如大小、修改时间等)
const stat = fs.statSync(filePath)
// 如果请求的是目录
if (stat.isDirectory()) {
// 将路径更新为目录下的 index.html 文件
filePath = path.resolve(filePath, 'index.html')
}
// 再次检查文件(index.html 或原始文件)是否存在
if (fs.existsSync(filePath)) {
// 解析文件路径,获取文件扩展名(如 .html、.jpg)
const { ext } = path.parse(filePath)
res.writeHead(200, {
'Content-Type': mime.getType(ext),
})
const fileStream = fs.createReadStream(filePath)//不是一次性读取,而是流式读取,每次读取一部分,然后返回给前端
fileStream.pipe(res)
}else {
// 如果 index.html 不存在,返回 404 状态码
res.writeHead(404, { 'Content-Type': 'text/html;charset=utf-8' })
// 发送简单的 404 错误页面
res.end('<h1>404 not found</h1>')
}
} else {
// 如果文件或目录不存在,返回 404 状态码
res.writeHead(404, { 'Content-Type': 'text/html;charset=utf-8' })
// 发送简单的 404 错误页面
res.end('<h1>404 not found</h1>')
}
})
// 启动服务器,监听 3000 端口
server.listen(3000, () => {
// 服务器启动后打印提示信息
console.log('server is running at http://localhost:3000')
})
如果要实现强缓存,则直接在请求头中加入'Cache-Control': 'max-age=86400'即可,'max-age=86400表示缓存的时间,以秒为单位。
res.writeHead(200, {
'Content-Type': mime.getType(ext),
'Cache-Control': 'max-age=86400',//缓存时间,单位是秒
//缓存到浏览器的Application中的 Cache Storage
})
强缓存虽然简单,减少了服务器请求,但缺点是可能返回过时资源、缺乏灵活性、带宽效率较低和缓存控制困难。协商缓存通过验证确保资源新鲜度,适合动态或频繁更新的内容。
什么是协商缓存?
协商缓存:先确认再使用
-
场景:朋友说:"借书前先问问我,如果 书没变 你就继续看旧版"
-
过程:
-
浏览器:携带"书籍信息"(书签)问服务器
-
服务器:检查资源是否变化
-
结果:
-
没变化 → 返回
304 Not Modified(继续看旧书) -
有变化 → 返回
200+ 新资源(给新书)
-
- 特征:
-
必须通信(总要问一句)
-
更精准(保证内容最新)
简单来说,就是浏览器每次要资源时都会请求服务器,而服务器会判断浏览器请求的文件资源是否修改,如果修改了就返回新的资源给浏览器,如果没有修改则叫浏览器去缓存中取(非强制)。
协商缓存有两种实现方式:
- 使用Last-Modified和If-Modified-Since
- 使用ETag和If-None-Match
Last-Modified和If-Modified-Since
Last-Modified表示文件最后一次的修改时间
实现思路:服务器在响应头上添加last-modified属性,值为文件最后一次更新的时间,浏览器拿到后,下一次请求则会在请求头上加入if-modified-since属性,值为上次请求时文件最后一次修改的时间,服务器拿到后则将其与文件再次最后修改的时间进行比较,如果不相同,则返回新的资源以及新的Last-Modified给浏览器,如果相同则返回304状态码,告诉浏览器自己的缓存找。
以下为部分代码
if (fs.existsSync(filePath)) {//再次判断访问的文件是否存在
const { ext } = path.parse(filePath)//获取文件的后缀名
const stat=fs.statSync(filePath)
//使用Last-Modified和If-Modified-Since
const timeStamp = req.headers['if-modified-since']
//获取前端请求头中的'if-modified-since'字段
let status = 200
if (timeStamp && Number(timeStamp) === stat.mtimeMs) {
//如果前端请求头中的'if-modified-since'字段与文件的最后修改时间相同,
// 说明文件未被修改,直接返回304状态码,告诉前端资源未被修改,浏览器会直接从缓存中获取资源
status = 304//迫使浏览器从缓存中获取资源
}
res.writeHead(status, {
'Content-Type': mime.getType(ext),
'Cache-Control': 'max-age=86400',//缓存时间,单位是秒
//缓存到浏览器的Application中的 Cache Storage
'last-modified':stat.mtimeMs,//文件最后修改时间,告诉前端图片更新了,
// 前端再次请求时,会在请求头中添加'if-modified-since':stat.mtimeMs,
})
if (status === 200) {
//一次性将文件全部读完
const fileStream = fs.createReadStream(filePath)//不是一次性读取,而是流式读取,每次读取一部分,然后返回给前端
fileStream.pipe(res)
} else {//如果状态码是304,说明文件未被修改,什么都不返回
res.end()
}
}else {
res.writeHead(404, { 'Content-Type': 'text/html;charset=utf-8' })
res.end('<h1>404 not found</h1>')
}
但是这种方式会出现一个问题,如果该文件第一次被修改后,感觉不适合,第二次又被修改回来了,那这样浏览器服务器还是会认为该文件修改了,所以给浏览器返回新的资源,但其实该文件并没有修改,这岂不是多此一举。那么我们就考虑用第二种方式:ETag和If-None-Match。
ETag和If-None-Match
实现思路:借助node.js的第三方模块checksum,生成每次更新文件的校验和(通常是哈希值),作为文件指纹(ETag),服务器将其添加到响应头,每次浏览器请求资源时,请求头中带有If-None-Match属性,值为上一次生成的文件指纹,服务器那其与这次生成的文件指纹(ETag)进行比较,如果不相同则返回新的资源以及新的ETag给浏览器,如果相同则返回304状态码,告诉浏览器取缓存中取。
以下为部分代码
// 再次检查文件(index.html 或原始文件)是否存在
if (fs.existsSync(filePath)) {
// 解析文件路径,获取文件扩展名(如 .html、.jpg)
const { ext } = path.parse(filePath)
// 再次获取文件详细信息,确保使用最新的文件元数据
const stat = fs.statSync(filePath)
// 计算文件的校验和(ETag),异步操作
checksum.file(filePath, (err, sum) => {
// 创建文件的读取流,用于流式传输文件内容
const resStream = fs.createReadStream(filePath)
// 将校验和包装为带引号的字符串,符合 ETag 格式(如 "abc123")
sum = `"${sum}"`
// 检查客户端请求头中的 If-None-Match 是否与文件的 ETag 匹配
if (req.headers['if-none-match'] === sum) {
// 如果匹配,说明文件未修改,返回 304 状态码
res.writeHead(304, {
// 设置响应内容的 MIME 类型,如 text/html
'Content-Type': mime.getType(ext),
// 设置缓存有效期为 86400 秒(1 天)
'Cache-Control': 'max-age=86400',
// 返回文件的 ETag 值
'etag': sum
})
// 结束响应,不发送文件内容,浏览器将使用缓存
res.end()
} else {
// 如果不匹配,说明文件已修改或首次请求,返回 200 状态码
res.writeHead(200, {
// 设置响应内容的 MIME 类型
'Content-Type': mime.getType(ext),
// 设置缓存有效期为 86400 秒(1 天)
'Cache-Control': 'max-age=86400',
// 返回文件的 ETag 值
'etag': sum
})
// 将文件内容通过流传输到客户端
resStream.pipe(res)
}
})
}
} else {
// 如果文件或目录不存在,返回 404 状态码
res.writeHead(404, { 'Content-Type': 'text/html;charset=utf-8' })
// 发送简单的 404 错误页面
res.end('<h1>404 not found</h1>')
}
小结:
| 特性 | 强缓存 | 协商缓存 |
|---|---|---|
| 优点 | - 减少请求,资源获取快(max-age=86400 1 天内无请求) - 实现简单,代码量少 | - 确保资源新鲜(验证 ETag 或 Last-Modified) - 节省带宽(304 响应) - 灵活性高 |
| 缺点 | - 可能返回过时资源(如 index.html 更新后仍用缓存) - 灵活性差(统一 1 天缓存) | - 增加请求开销(每次验证) - ETag 计算开销大(checksum.file) - 实现复杂 |
| 代码实现 | Cache-Control: max-age=86400 | ETag(checksum.file)或 Last-Modified(stat.mtimeMs) |
| 适用场景 | 适合不频繁更新的资源(如图片、CSS) | 适合频繁更新的资源(如 index.html) |