浏览器缓存机制

98 阅读5分钟

浏览器缓存机制

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

浏览器缓存-http-缓存.png

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 的高

浏览器缓存-cache-control-原理.png

// 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>

浏览器请求相关信息:

浏览器缓存-cache-control.png

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>

浏览器缓存-expires.png

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 请求,用于安全地执行资源更新操作。

浏览器缓存-ETag-原理.png

// 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>

浏览器缓存-ETag.png

[!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, 让浏览器使用缓存的副本,从而节省带宽和提高加载速度。

浏览器缓存-last-modified-原理.png

// 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>

浏览器缓存-last-modified.png

[!caution]

注意到 返回信息与 ETag 返回的不相同,Last-modified 会显示服务端返回的 304 状态码