浏览器缓存机制
http 标准里并没有明确规定什么强缓存或者弱缓存,只是程序员为了方便理解造出的名词而已 所谓的 强弱缓存 只是 浏览器 和 业务侧 根据不同的请求报文做出不同的 缓存动作而已
http 缓存字段: Cache-Control: 搭配 max-age 使用,在请求头设置 Cache-Control:max-age 在 max-age 时间内再次请求会读取缓存数据(dist cache) Expires: 设置过期时间,在请求头设置 expires: 2023-02-23 Etag / If-None-Match 服务端判断自己的 Etag 是否和 客户端请求头里带的 Etag 是否一样,一样返回 304, 不一样返回新数据 Last-modified / If-Modified-Since: 记录最后一次修改的时间,服务端判断浏览器的最后一次修改时间之后是否有更新,有更新则返回新的数据,没有更新则返回 304
Cache-Control
Cache Control 是个用来控制缓存行为的 HTTP响应头字段。通过设置不同的Cache -Control指令,您可以控制浏览器对特定资源的缓存方式。以下是一些常用的Cache -Control指令及其作用:
public:表示响应可以被客户端和中间代理服务器缓存。
private:表示响应只能被客户端缓存,中间代理服务器不能缓存该响应。
no-cache:表示客户端缓存资源,但在使用缓存前必须先向服务器验证其有效性。
no-store:表示禁止缓存该响应,每次都要向服务器请求资源。
max- age=:指定资源在客户端缓存的最大时间(以秒为单位)。
s-maxage=:类似于max-age,但只适用于共享代理服务器缓存。
[!note]
传参不同缓存会失效, Expires 也是
cache-control 的优先级比 Expires 的高
// NODEJS
import express from 'express'
const router = express.Router()
router.get('/out/browser-cache/control-cache', (req, res) => {
setTimeout(() => {
res.setHeader('Cache-Control', 'public, max-age=5') // 设置缓存时间为5秒
res.send('control-cache 返回数据')
}, 500)
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>浏览器缓存</title>
</head>
<body>
<button id="control-cache">control-cache</button>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
// control-cache
const controlCacheBtn = document.querySelector("#control-cache");
controlCacheBtn.addEventListener("click", function () {
const url = "http://127.0.0.1:3000/api/out/browser-cache/control-cache";
axios.get(url).then(function (res) {
console.log(res);
})
});
</script>
</body>
</html>
浏览器请求相关信息:
Expires
Expires: (缓存到期日期)
原理同 cache-control
// NODEJS
import express from 'express'
const router = express.Router()
router.get('/out/browser-cache/expires', (req, res) => {
setTimeout(() => {
// 设置缓存过期时间 5秒后
res.setHeader('Expires', new Date(Date.now() + 5 * 1000).toUTCString())
res.send('expires 返回数据')
}, 500)
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>浏览器缓存</title>
</head>
<body>
<button id="control-cache">control-cache</button>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
// expires
const expiresBtn = document.querySelector("#expires");
expiresBtn.addEventListener("click", function () {
const url = "http://127.0.0.1:3000/api/out/browser-cache/expires";
axios.get(url).then(function (res) {
console.log(res);
});
});
</script>
</body>
</html>
Etag / If-None-Match
- Etag缓存是种用于缓存管理的HTTP标头。服务器可以在响应中发送Etag标记,表示响应实体的特定版本。当客户端再次请求相同资源时,它可以将这个Etag值包含在请求中。服务器可以检查Etag值,如果资源的Etag值和客户端发送的匹配,服务器可以返回状态码304 (未修改)而不是完整的资源内容,从而节省带宽和加快加载时间。
- Etag 值通常是基于资源内容生成的散列值,可以确保资源的实际内容是否发生变化。
- If-None-Match: 客户端发送此头部字段来检查资源是否已更新。服务器端会将资源的ETag (实体标签) 与客户端发送的 If-None-Match 进行比较,如果一致(即资源没有更新),服务器会返回状态码 304 Not Modified, 告知客户端继续使用缓存中的资源。
- If-Match: 客户端发送此头部字段来执行部分更新请求,即在更新资源时,只有当请求头部中的If- None-Match与服务器端资源的 ETag 匹配时,才会进行更新。如果不匹配,服务器将返回状态码 412 Precondition Failed, 表示无法更新。
- If- None-Match 常用于条件 GET 请求,用于判断资源是否更新; 而 If-Match 常用于 条件 PUT 或 PATCH 请求,用于安全地执行资源更新操作。
// NODEJS
router.get('/out/browser-cache/Etag', (req, res) => {
// 生成 ETag (Etag 值通常是基于资源内容生成的散列值)
// 这里为了测试,直接返回一个固定的值
const resourceETag = md5('1234567890')
// 检查客户端发送的 If-None-Match 头部字段
const requestEtag = req.headers['if-none-match']
// 如果服务端的 ETag 和 客户端的 If-None-Match 头部字段匹配,则表示资源未修改,返回 304 状态码
if(requestEtag === resourceETag) {
setTimeout(() => {
// 设置响应状态码为 304,表示请求的资源未修改,可以直接使用客户端缓存的版本
res.status(304).send('304 资源未修改')
}, 500)
} else {
setTimeout(() => {
res.setHeader('ETag', resourceETag) // 设置缓存标识
// 将 ETag 暴露给前端
res.setHeader("Access-Control-Expose-Headers", "ETag, If-None-Match")
res.send(`${dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss')} etag 返回数据`)
}, 500)
}
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>浏览器缓存</title>
</head>
<body>
<button id="control-cache">control-cache</button>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
// etag
const etagBtn = document.querySelector("#etag");
etagBtn.addEventListener("click", function () {
const ETagItem = sessionStorage.getItem("ETagItem")
const url = "http://127.0.0.1:3000/api/out/browser-cache/Etag";
axios.get(url, {
headers: {
"If-None-Match": ETagItem ? ETagItem.etag : null,
// 将 If-None-Match 暴露给后端
"Access-Control-Expose-Headers": "If-None-Match",
}
}).then(function (res) {
console.log("res", res);
sessionStorage.setItem("ETagItem", JSON.stringify(res.headers.etag));
}).catch(function (res) {
console.log("catch", res);
});
});
</script>
</body>
</html>
[!caution]
如上图所示,再次发送请求时 服务端判断到 If-None-Match 和 ETag 相等并将状态码设置为 304,但是此时浏览器并没有显示 304 而是 200 并读取缓存数据(如果设为其他状态码就不会出现这种请求,比如404,浏览器会直接报 404)。但是如果此时 清空浏览器缓存 但是 If-None-Match 保持不变(即资源没有更新),服务端会返回 304,这时浏览器也会显示 304 状态码。
上述现象在 postman 中不会出现(即如果 If-None-Match 和 ETag 相等,则状态码为 304),所以猜测可能浏览器做了优化,有缓存就读取缓存且状态码为200,读取不到缓存才会报 304,而不是单纯的显示服务端状态码
Last-modified / If-Modified-Since
- Last Modified 是服务器响应的HTTP头部字段,指示服务器资源的最后修改时间。当浏览器请求一个资源时,服务器会返回这个资源的最后修改时间。浏览器会保存这个值,下次请求资源时会在请求头中包含 If-Modified-Since 字段,询问服务器这个时间之后资源有没有修改过。
- If- Modified- Since 是浏览器在请求资源时包含的HTTP头部字段,用于告诉服务器资源的最后修改时间。服务器会根据这个值判断资源自从这个时间之后有没有进行过修改。如果没有修改过,服务器会返回状态码304 Not Modified, 让浏览器使用缓存的副本,从而节省带宽和提高加载速度。
// NODEJS
// 模拟缓存 last-modified
// 最后一次修改时间
const lastModified = `${dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss')}`
router.get('/out/browser-cache/last-modified', (req, res) => {
console.log('接收到了请求 last-modified')
const ifModifiedSince = req.headers['if-modified-since']
if(lastModified === ifModifiedSince) {
// 设置响应状态码为 304,表示请求的资源未修改,可以直接使用客户端缓存的版本
res.status(304).send('304 资源未修改')
} else {
setTimeout(() => {
// 设置最后一次修改的时间
res.setHeader('Last-Modified', lastModified)
res.send(`${dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss')} last-modified 返回数据`)
}, 500)
}
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>浏览器缓存</title>
</head>
<body>
<button id="control-cache">control-cache</button>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
// last-modified
const lastModifiedBtn = document.querySelector("#last-modified");
lastModifiedBtn.addEventListener("click", function () {
const url = "http://127.0.0.1:3000/api/out/browser-cache/last-modified";
axios.get(url, {
headers: {
"If-Modified-Since": JSON.parse(window.sessionStorage.getItem("lastModified")) || null,
// 将 If-Modified-Since 暴露给后端
"Access-Control-Expose-Headers": "If-Modified-Since",
}
}).then(function (res) {
console.log("res", res);
window.sessionStorage.setItem("lastModified", JSON.stringify(res.headers['last-modified']));
});
});
</script>
</body>
</html>
[!caution]
注意到 返回信息与 ETag 返回的不相同,Last-modified 会显示服务端返回的 304 状态码