【HTTP缓存机制深度解析:从ETag到实践策略】

发布于:2025-07-22 ⋅ 阅读:(19) ⋅ 点赞:(0)

HTTP缓存机制深度解析:从ETag到实践策略

目录

  1. ETag协商缓存机制
  2. HTTP缓存的完整判断流程
  3. 缓存策略的选择原则
  4. HTML文件的缓存策略分析
  5. 浏览器缓存的实现机制
  6. 私有缓存机制详解
  7. HTML文件命名策略的深度思考
  8. 最佳实践与架构建议

1. ETag协商缓存机制

1.1 ETag的本质与生成方式

ETag(Entity Tag)是HTTP协议中用于协商缓存的核心机制,它通过为资源生成唯一标识符来判断内容是否发生变化。

常见的ETag生成方式:

// 1. 内容哈希值
const crypto = require('crypto');
const etag = crypto.createHash('md5').update(fileContent).digest('hex');

// 2. 文件修改时间 + 大小
const etag = `${file.mtime.getTime()}-${file.size}`;

// 3. 版本号
const etag = `v${packageVersion}`;

// 4. Nginx默认方式
// ETag = 文件修改时间的十六进制 + "-" + 文件大小的十六进制

1.2 ETag的完整工作流程

首次请求:
客户端 → 服务器:GET /resource.js
服务器 → 客户端:200 OK
                 ETag: "abc123"
                 Content: [文件内容]

后续请求(协商缓存):
客户端 → 服务器:GET /resource.js
                 If-None-Match: "abc123"

服务器判断:
- 计算当前资源的ETag
- 与客户端发送的ETag比较

情况1:ETag匹配(资源未变化)
服务器 → 客户端:304 Not Modified
                 ETag: "abc123"
客户端使用本地缓存的资源。

情况2:ETag不匹配(资源已变化)
服务器 → 客户端:200 OK
                 ETag: "def456"
                 Content: [新的文件内容]
客户端接收新资源并更新缓存。

1.3 强ETag vs 弱ETag

# 强ETag - 精确匹配,任何字节变化都会改变
ETag: "abc123"

# 弱ETag - 语义等价匹配,允许不重要的变化
ETag: W/"abc123"

比较规则:

  • 强ETag之间:必须完全相同
  • 弱ETag之间:语义相同即可
  • 强弱混合:按弱ETag规则比较

1.4 协商缓存的网络开销分析

重要结论:协商缓存必定产生网络请求

客户端 → 服务器:请求 + 缓存标识
服务器 → 客户端:304 Not Modified(无内容体)或 200 OK(完整内容)

虽然304响应没有内容体,但仍有网络开销:

  • TCP连接建立的延迟
  • HTTP请求/响应头的传输
  • 网络往返时间(RTT)

2. HTTP缓存的完整判断流程

2.1 缓存判断的优先级顺序

发起HTTP请求
    ↓
本地是否有缓存?
    ├─ 无缓存 → 直接请求服务器
    └─ 有缓存 → 强缓存是否有效?
                ├─ 有效 → 直接使用本地缓存(200 from cache)
                └─ 无效/不存在 → 是否有协商缓存标识?
                                ├─ 无 → 直接请求服务器
                                ├─ 有ETag → 发送 If-None-Match
                                └─ 有Last-Modified → 发送 If-Modified-Since
                                                    ↓
                                                服务器判断
                                                ├─ 资源未变化 → 304 Not Modified(使用本地缓存)
                                                └─ 资源已变化 → 200 OK(返回新资源)

2.2 强缓存判断逻辑

// 浏览器内部强缓存判断逻辑(伪代码)
function checkStrongCache(request) {
    const cachedResponse = getFromCache(request.url);
    if (!cachedResponse) return null;
    
    // 1. 检查 Cache-Control
    if (cachedResponse.headers['cache-control']) {
        const directives = parseCacheControl(cachedResponse.headers['cache-control']);
        
        // no-cache: 跳过强缓存,直接协商
        if (directives.includes('no-cache')) return null;
        
        // max-age: 检查是否过期
        if (directives['max-age']) {
            const age = (Date.now() - cachedResponse.timestamp) / 1000;
            if (age < directives['max-age']) {
                return cachedResponse; // 强缓存命中
            }
        }
    }
    
    // 2. 检查 Expires(优先级低于Cache-Control)
    if (cachedResponse.headers['expires']) {
        const expiresTime = new Date(cachedResponse.headers['expires']);
        if (Date.now() < expiresTime.getTime()) {
            return cachedResponse; // 强缓存命中
        }
    }
    
    return null; // 强缓存失效,进入协商缓存
}

2.3 协商缓存判断逻辑

function buildNegotiationRequest(request, cachedResponse) {
    const headers = { ...request.headers };
    
    // 优先使用 ETag
    if (cachedResponse.headers['etag']) {
        headers['If-None-Match'] = cachedResponse.headers['etag'];
    }
    
    // 同时使用 Last-Modified(向后兼容)
    if (cachedResponse.headers['last-modified']) {
        headers['If-Modified-Since'] = cachedResponse.headers['last-modified'];
    }
    
    return { ...request, headers };
}

3. 缓存策略的选择原则

3.1 核心决策因子

因子 强缓存 协商缓存 禁用缓存
内容变化频率 几乎不变 偶尔变化 频繁变化
实时性要求 中等
版本控制 有(哈希/版本号) 不适用
文件类型 静态资源 HTML/API 敏感数据
更新方式 文件名变化 内容变化 实时更新

3.2 决策树模型

选择缓存策略时的决策流程:

1. 内容会变化吗?
   ├─ 几乎不变 → 强缓存(1年)
   ├─ 偶尔变化 → 协商缓存
   └─ 频繁变化 → 短期缓存或禁用缓存

2. 实时性要求高吗?
   ├─ 要求高 → 协商缓存或禁用缓存
   └─ 要求低 → 强缓存

3. 有版本控制吗?
   ├─ 有版本号/哈希 → 强缓存
   └─ 无版本控制 → 协商缓存

4. 是否为入口文件?
   ├─ 是(HTML) → 协商缓存
   └─ 否(资源文件) → 强缓存

3.3 典型场景的缓存配置

静态资源(强缓存)
# CSS、JS、图片等
Cache-Control: public, max-age=31536000, immutable
HTML文件(协商缓存)
# 入口HTML文件
Cache-Control: no-cache
ETag: "abc123"
API接口(灵活配置)
# 根据业务需求调整
Cache-Control: private, max-age=300  # 5分钟
敏感数据(禁用缓存)
# 用户个人信息、实时数据
Cache-Control: no-store, no-cache, must-revalidate
私有缓存(用户个人数据)
# 用户相关的个人数据
Cache-Control: private, max-age=300
Vary: Authorization

3.4 must-revalidate指令详解

must-revalidate是Cache-Control中的一个重要指令,它对缓存行为有严格的控制要求。

must-revalidate的工作机制

重要澄清:must-revalidate 不会影响强缓存的正常工作!

# 基本用法
Cache-Control: max-age=3600, must-revalidate

# 与其他指令组合
Cache-Control: private, max-age=300, must-revalidate

# 完全禁用缓存的组合
Cache-Control: no-store, no-cache, must-revalidate
完整的缓存判断流程
// 浏览器处理 must-revalidate 的完整逻辑
function handleMustRevalidateCache(request) {
    const cachedResponse = getFromCache(request.url);
    if (!cachedResponse) {
        return fetch(request); // 无缓存,正常请求
    }

    const cacheControl = cachedResponse.headers['cache-control'];
    const maxAge = parseCacheControl(cacheControl)['max-age'];
    const age = (Date.now() - cachedResponse.timestamp) / 1000;

    // 关键:先检查是否过期
    if (age < maxAge) {
        // ✅ 未过期:强缓存正常工作,直接返回缓存
        console.log('强缓存命中,直接使用缓存');
        return cachedResponse;
    } else {
        // ❌ 已过期:must-revalidate 开始起作用
        if (cacheControl.includes('must-revalidate')) {
            console.log('缓存过期,must-revalidate 要求必须验证');
            return fetch(request); // 必须向服务器验证
        } else {
            console.log('缓存过期,但可能使用过期缓存作为降级');
            // 普通缓存可能在某些情况下返回过期缓存
            return fetch(request).catch(() => cachedResponse);
        }
    }
}
时间线示例
# 服务器响应
Cache-Control: max-age=3600, must-revalidate

访问时间线:

T=0秒:   首次请求 → 网络请求 → 缓存3600秒
T=1800秒:用户访问 → ✅ 强缓存命中,直接使用缓存(无网络请求)
T=2400秒:用户访问 → ✅ 强缓存命中,直接使用缓存(无网络请求)
T=3600秒:用户访问 → ❌ 缓存过期,must-revalidate 要求必须验证
T=3601秒:发起网络请求进行验证
弱网环境下的缓存行为

1. 普通缓存在弱网环境的行为

// 普通缓存:可能使用过期缓存作为降级
function normalCacheWithStaleWhileRevalidate(request, cachedResponse) {
    if (cachedResponse.isExpired()) {
        // 尝试网络请求
        return fetch(request)
            .then(response => {
                // 网络成功,更新缓存
                updateCache(request, response);
                return response;
            })
            .catch(error => {
                // 网络失败,返回过期缓存作为降级
                console.log('网络失败,使用过期缓存');
                return cachedResponse;
            });
    }
    return cachedResponse;
}

2. must-revalidate 在弱网环境的严格行为

// must-revalidate:绝不使用过期缓存
function mustRevalidateStrictBehavior(request, cachedResponse) {
    if (cachedResponse.isExpired()) {
        return fetch(request)
            .then(response => {
                updateCache(request, response);
                return response;
            })
            .catch(error => {
                // 网络失败时,必须返回错误,不能使用过期缓存
                console.log('网络失败,must-revalidate 禁止使用过期缓存');
                throw new Error('504 Gateway Timeout');
            });
    }
    return cachedResponse;
}
浏览器的实际降级策略

现代浏览器在弱网环境下确实可能使用过期缓存:

Chrome的stale-while-revalidate行为:

// Chrome在某些情况下的行为
function chromeStaleWhileRevalidate(request, cachedResponse) {
    if (cachedResponse.isExpired() &&
        !cachedResponse.headers['cache-control'].includes('must-revalidate')) {

        // 1. 立即返回过期缓存给用户(提升体验)
        const staleResponse = cachedResponse.clone();

        // 2. 后台发起网络请求更新缓存
        fetch(request)
            .then(freshResponse => updateCache(request, freshResponse))
            .catch(error => console.log('后台更新失败'));

        return staleResponse;
    }
}

Service Worker的离线降级:

// Service Worker 可能的离线策略
self.addEventListener('fetch', event => {
    event.respondWith(
        fetch(event.request)
            .catch(() => {
                // 网络失败时的降级策略
                return caches.match(event.request)
                    .then(cachedResponse => {
                        if (cachedResponse) {
                            const cacheControl = cachedResponse.headers.get('cache-control');

                            if (cacheControl?.includes('must-revalidate')) {
                                // must-revalidate 禁止使用过期缓存
                                return new Response('Network Error', { status: 504 });
                            } else {
                                // 普通缓存可以使用过期版本
                                return cachedResponse;
                            }
                        }
                        return new Response('Not Found', { status: 404 });
                    });
            })
    );
});
实际应用场景

1. 金融数据

app.get('/api/account/balance', authenticateUser, (req, res) => {
    const balance = getAccountBalance(req.user.id);

    res.set({
        'Cache-Control': 'private, max-age=60, must-revalidate',
        'Vary': 'Authorization'
    });

    res.json({ balance, timestamp: Date.now() });
});

2. 医疗记录

app.get('/api/medical/records', authenticateUser, (req, res) => {
    const records = getMedicalRecords(req.user.id);

    res.set({
        'Cache-Control': 'private, max-age=300, must-revalidate',
        'Vary': 'Authorization'
    });

    res.json(records);
});

3. 实时库存

app.get('/api/inventory/:productId', (req, res) => {
    const inventory = getInventory(req.params.productId);

    res.set({
        'Cache-Control': 'public, max-age=30, must-revalidate'
    });

    res.json({
        productId: req.params.productId,
        stock: inventory.stock,
        lastUpdated: inventory.lastUpdated
    });
});
must-revalidate vs 其他指令的对比
指令 缓存有效期内 过期后行为 弱网/离线时 适用场景
max-age=3600 ✅ 强缓存生效 可能使用过期缓存 可能返回过期缓存 一般数据
no-cache ❌ 总是验证 总是验证 可能返回过期缓存 需要验证的数据
must-revalidate ✅ 强缓存生效 必须验证 返回错误,不用过期缓存 关键数据
no-store ❌ 不缓存 不缓存 不缓存 敏感数据
弱网环境下的缓存降级机制

现代浏览器和应用在弱网环境下确实会使用过期缓存作为降级策略:

1. 浏览器的自动降级

// 浏览器可能的降级逻辑
function browserStaleStrategy(request, cachedResponse) {
    if (cachedResponse.isExpired()) {
        const cacheControl = cachedResponse.headers['cache-control'];

        // 检查网络状况
        if (navigator.connection?.effectiveType === 'slow-2g' ||
            navigator.connection?.downlink < 0.5) {

            if (!cacheControl.includes('must-revalidate')) {
                console.log('弱网环境,使用过期缓存提升用户体验');
                return cachedResponse; // 使用过期缓存
            }
        }
    }
}

2. stale-while-revalidate 策略

# 明确指定可以使用过期缓存的时间窗口
Cache-Control: max-age=3600, stale-while-revalidate=86400
// stale-while-revalidate 的实现
function staleWhileRevalidate(request, cachedResponse) {
    const maxAge = 3600; // 1小时
    const staleTime = 86400; // 24小时内可以使用过期缓存
    const age = (Date.now() - cachedResponse.timestamp) / 1000;

    if (age < maxAge) {
        // 新鲜缓存,直接使用
        return cachedResponse;
    } else if (age < maxAge + staleTime) {
        // 过期但在stale窗口内,可以使用过期缓存
        // 同时后台发起请求更新
        fetch(request).then(response => updateCache(request, response));
        return cachedResponse; // 立即返回过期缓存
    } else {
        // 完全过期,必须等待网络请求
        return fetch(request);
    }
}

3. PWA 离线策略

// PWA 应用的离线降级策略
self.addEventListener('fetch', event => {
    if (event.request.url.includes('/api/')) {
        event.respondWith(
            // 网络优先策略
            fetch(event.request)
                .then(response => {
                    // 成功时更新缓存
                    const responseClone = response.clone();
                    caches.open('api-cache').then(cache => {
                        cache.put(event.request, responseClone);
                    });
                    return response;
                })
                .catch(() => {
                    // 网络失败时的降级策略
                    return caches.match(event.request)
                        .then(cachedResponse => {
                            if (cachedResponse) {
                                const cacheControl = cachedResponse.headers.get('cache-control');

                                if (cacheControl?.includes('must-revalidate')) {
                                    // 严格模式:不使用过期缓存
                                    return new Response(
                                        JSON.stringify({ error: '网络连接失败,数据可能不是最新' }),
                                        { status: 504, headers: { 'Content-Type': 'application/json' } }
                                    );
                                } else {
                                    // 宽松模式:使用过期缓存,但添加警告
                                    const response = cachedResponse.clone();
                                    response.headers.set('X-Cache-Warning', 'stale-data');
                                    return response;
                                }
                            }
                            return new Response('离线且无缓存', { status: 404 });
                        });
                })
        );
    }
});
代理服务器的处理
// 代理服务器对 must-revalidate 的处理
function proxyHandleMustRevalidate(request, cachedResponse) {
    const cacheControl = cachedResponse.headers['cache-control'];

    if (cacheControl.includes('must-revalidate') &&
        cachedResponse.isExpired()) {

        try {
            // 必须向上游服务器验证
            return fetch(request, { upstream: true });
        } catch (networkError) {
            // 网络错误时返回 504
            return new Response('Gateway Timeout', { status: 504 });
        }
    }

    return cachedResponse;
}

4. HTML文件的缓存策略分析

4.1 为什么HTML使用协商缓存?

HTML文件的特殊性
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="/css/app.css?v=1.2.3">
    <script src="/js/app.js?v=1.2.3"></script>
</head>
<body>
    <!-- 页面内容可能经常更新 -->
    <div id="app"></div>
</body>
</html>
核心原因分析

1. 版本控制的入口点

  • HTML文件包含其他资源的版本信息
  • 必须确保用户获取到最新的资源引用
  • 避免新版本资源与旧版本HTML的不匹配

2. 内容更新频率

  • 页面结构、元数据可能频繁变化
  • 新功能发布需要更新HTML结构
  • A/B测试可能修改页面内容

3. 用户体验考量

  • 用户刷新页面期望看到最新内容
  • 强缓存会导致用户看不到重要更新
  • 协商缓存平衡了性能和实时性

4.2 HTML强缓存的问题分析

如果HTML使用强缓存会出现什么问题?

// 场景:发布新版本
// 旧版本HTML(被强缓存)
<script src="/js/app.v1.0.0.js"></script>

// 新版本资源已部署
/js/app.v2.0.0.js  // 新版本
/js/app.v1.0.0.js  // 已删除

// 结果:用户看到404错误

问题总结:

  • 资源引用不匹配
  • 功能缺失或错误
  • 用户体验严重受损
  • 需要手动清除缓存

4.3 内容哈希与HTML协商缓存的配合机制

现代前端项目通常使用内容哈希 + HTML协商缓存的组合策略,但很多开发者对这个机制存在误解。

协商缓存不存在"过期"概念
# HTML文件的协商缓存配置
Cache-Control: no-cache
ETag: "abc123"

重要澄清:协商缓存每次都会向服务器询问,不存在"没过期所以没更新"的情况:

用户访问 → 浏览器发送请求 + If-None-Match: "abc123"
服务器判断 → ETag变了吗?
├─ 没变 → 304 Not Modified(使用本地缓存)
└─ 变了 → 200 OK + 新的HTML内容(包含新的哈希引用)
打包部署的完整流程

1. 构建阶段的文件变化

# 构建前
src/
├── index.html
├── main.js
└── style.css

# 构建后
dist/
├── index.html                    # 内容包含新的哈希引用
├── js/
│   └── main.a1b2c3d4.js         # 新的哈希文件名
└── css/
    └── style.e5f6g7h8.css       # 新的哈希文件名

2. HTML内容的自动更新

<!-- 构建前的模板 -->
<!DOCTYPE html>
<html>
<head>
    <!-- Webpack会自动注入资源引用 -->
</head>
<body>
    <div id="app"></div>
</body>
</html>
<!-- 构建后的实际HTML -->
<!DOCTYPE html>
<html>
<head>
    <link href="/css/style.e5f6g7h8.css" rel="stylesheet">
</head>
<body>
    <div id="app"></div>
    <script src="/js/main.a1b2c3d4.js"></script>
</body>
</html>

3. 服务器ETag的自动更新

// 服务器生成ETag的逻辑
const fs = require('fs');
const crypto = require('crypto');

app.get('/', (req, res) => {
    const htmlContent = fs.readFileSync('./dist/index.html', 'utf8');
    const etag = crypto.createHash('md5').update(htmlContent).digest('hex');

    // 检查客户端ETag
    if (req.headers['if-none-match'] === `"${etag}"`) {
        return res.status(304).end(); // 内容没变
    }

    // HTML内容变了(包含新的哈希引用),返回新HTML
    res.set({
        'Cache-Control': 'no-cache',
        'ETag': `"${etag}"`
    });
    res.send(htmlContent);
});
为什么JS文件不需要must-revalidate

有些开发者担心JS文件的缓存问题,想给JS文件加上must-revalidate

# ❌ 不必要的配置
location ~* \.js$ {
    add_header Cache-Control "public, max-age=31536000, must-revalidate";
}

这是不必要的,原因:

  1. 文件名变化:内容哈希确保新版本有完全不同的文件名
  2. 浏览器行为:浏览器会请求新的文件名,不存在"过期"概念
  3. 性能损失:must-revalidate 会在缓存过期后强制验证,降低性能
// 内容哈希的工作原理
// 旧版本
app.abc123.js  // 强缓存1年

// 新版本(内容变化后)
app.def456.js  // 完全不同的文件名,浏览器会重新请求
常见的缓存问题及解决方案

问题1:部署不完整

# ❌ 错误的部署方式
# 只上传了新的JS文件,没有更新HTML
scp dist/js/main.a1b2c3d4.js server:/var/www/html/js/
# HTML文件还是旧的,引用旧的JS文件名

# ✅ 正确的部署方式
# 完整上传整个dist目录
rsync -av dist/ server:/var/www/html/

问题2:CDN缓存延迟

# 问题:CDN还缓存着旧的HTML文件
# 解决方案:部署后刷新CDN
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
     -H "Authorization: Bearer {api_token}" \
     -H "Content-Type: application/json" \
     --data '{"files":["https://example.com/index.html"]}'

问题3:构建配置错误

// ❌ 错误配置:哈希不够精确
module.exports = {
    output: {
        filename: '[name].[hash].js',  // 基于编译哈希,可能重复
    }
};

// ✅ 正确配置:基于文件内容的哈希
module.exports = {
    output: {
        filename: '[name].[contenthash].js',  // 基于文件内容,确保唯一
    }
};

问题4:服务器ETag配置

# ❌ 可能的问题:Nginx没有正确生成ETag
location ~* \.html$ {
    add_header Cache-Control "no-cache";
    # 缺少ETag配置
}

# ✅ 正确配置:确保ETag基于文件内容
location ~* \.html$ {
    add_header Cache-Control "no-cache";
    etag on;  # 启用ETag,基于文件内容和修改时间
}
完整的部署验证脚本
#!/bin/bash
# deploy.sh - 确保部署正确性

echo "开始构建..."
npm run build

echo "检查构建结果..."
NEW_JS_HASH=$(cat dist/index.html | grep -o 'main\.[a-f0-9]*\.js' | head -1)
echo "新的JS文件: $NEW_JS_HASH"

echo "部署到服务器..."
rsync -av --delete dist/ server:/var/www/html/

echo "验证部署结果..."
DEPLOYED_JS=$(ssh server "cat /var/www/html/index.html | grep -o 'main\.[a-f0-9]*\.js' | head -1")
echo "服务器上的JS文件: $DEPLOYED_JS"

if [ "$NEW_JS_HASH" = "$DEPLOYED_JS" ]; then
    echo "✅ 部署成功,哈希匹配"
else
    echo "❌ 部署失败,哈希不匹配"
    exit 1
fi

echo "刷新CDN缓存..."
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
     -H "Authorization: Bearer $API_TOKEN" \
     -H "Content-Type: application/json" \
     --data '{"files":["https://your-site.com/index.html"]}'

echo "部署完成!"
调试缓存问题的方法
// 在浏览器控制台检查当前状态
console.log('当前加载的JS文件:');
Array.from(document.scripts).forEach(script => {
    console.log(script.src);
});

// 检查HTML的缓存状态
fetch('/', { method: 'HEAD' })
    .then(response => {
        console.log('HTML Cache-Control:', response.headers.get('cache-control'));
        console.log('HTML ETag:', response.headers.get('etag'));
        console.log('HTML Last-Modified:', response.headers.get('last-modified'));
    });

// 检查网络面板
// - 查看HTML请求是否返回304(协商缓存命中)
// - 查看JS文件是否从缓存加载(from disk cache)

5. 浏览器缓存的实现机制

5.1 缓存存储结构

// 浏览器内部缓存结构(简化模型)
const browserCache = {
    // HTTP缓存
    httpCache: new Map([
        ['https://example.com/app.js', {
            response: {
                status: 200,
                headers: new Headers({
                    'content-type': 'application/javascript',
                    'cache-control': 'max-age=3600',
                    'etag': '"abc123"',
                    'last-modified': 'Wed, 21 Oct 2023 07:28:00 GMT'
                }),
                body: '/* JavaScript content */'
            },
            timestamp: 1698742080000,
            size: 1024,
            accessCount: 5
        }]
    ]),

    // 内存缓存(最快访问)
    memoryCache: new Map(),

    // 磁盘缓存(持久化)
    diskCache: new Map(),

    // Service Worker缓存
    serviceWorkerCache: new Map()
};

5.2 缓存查找算法

function findCache(url, method = 'GET', headers = {}) {
    // 1. 构建缓存键
    const cacheKey = buildCacheKey(url, method, headers);

    // 2. 按优先级查找
    let cached = memoryCache.get(cacheKey);      // 内存缓存(最快)
    if (cached) return cached;

    cached = diskCache.get(cacheKey);            // 磁盘缓存
    if (cached) {
        // 提升到内存缓存
        memoryCache.set(cacheKey, cached);
        return cached;
    }

    cached = serviceWorkerCache.get(cacheKey);   // SW缓存
    if (cached) return cached;

    return null; // 无缓存
}

function buildCacheKey(url, method, headers) {
    const normalizedUrl = new URL(url).href;
    const varyHeaders = extractVaryHeaders(headers);

    return {
        url: normalizedUrl,
        method: method.toUpperCase(),
        vary: varyHeaders
    };
}

5.3 URL匹配规则

// 缓存匹配的严格规则
const examples = {
    // ✅ 精确匹配
    same: [
        'https://example.com/app.js',
        'https://example.com/app.js'
    ],

    // ❌ 查询参数敏感
    different: [
        'https://example.com/app.js?v=1',
        'https://example.com/app.js?v=2'
    ],

    // ❌ 协议敏感
    protocolDiff: [
        'http://example.com/app.js',
        'https://example.com/app.js'
    ],

    // ❌ 端口敏感
    portDiff: [
        'https://example.com:8080/app.js',
        'https://example.com/app.js'
    ],

    // ❌ 大小写敏感(路径部分)
    caseDiff: [
        'https://example.com/App.js',
        'https://example.com/app.js'
    ]
};

5.4 Vary头的影响

Vary头是HTTP缓存机制中的一个重要概念,它告诉缓存系统应该根据哪些请求头的值来区分不同的缓存条目。

Vary头的工作原理
# 服务器响应
HTTP/1.1 200 OK
Cache-Control: max-age=3600
Vary: Accept-Encoding, User-Agent
Content-Encoding: gzip

Vary头的含义:

  • 告诉缓存系统:相同URL的请求,如果Accept-EncodingUser-Agent不同,应该视为不同的资源
  • 缓存系统会为每种组合创建独立的缓存条目
  • 确保不同客户端获得适合的响应内容
Vary头的实际应用场景
// 1. 内容协商 - 根据Accept-Encoding提供不同压缩格式
app.get('/api/data', (req, res) => {
    const data = getData();

    res.set({
        'Cache-Control': 'public, max-age=3600',
        'Vary': 'Accept-Encoding'  // 根据压缩支持提供不同版本
    });

    // 根据客户端支持的压缩格式返回
    if (req.headers['accept-encoding']?.includes('br')) {
        res.set('Content-Encoding', 'br');
        res.send(compressBrotli(data));
    } else if (req.headers['accept-encoding']?.includes('gzip')) {
        res.set('Content-Encoding', 'gzip');
        res.send(compressGzip(data));
    } else {
        res.send(data);
    }
});

// 2. 用户认证 - 根据Authorization提供不同内容
app.get('/api/user/dashboard', (req, res) => {
    const userDashboard = getDashboard(req.user);

    res.set({
        'Cache-Control': 'private, max-age=300',
        'Vary': 'Authorization'  // 确保不同用户的数据不会混淆
    });

    res.json(userDashboard);
});

// 3. 设备适配 - 根据User-Agent提供不同版本
app.get('/api/content', (req, res) => {
    const userAgent = req.headers['user-agent'];
    const isMobile = /Mobile|Android|iPhone/i.test(userAgent);

    res.set({
        'Cache-Control': 'public, max-age=1800',
        'Vary': 'User-Agent'  // 移动端和桌面端不同内容
    });

    if (isMobile) {
        res.json(getMobileContent());
    } else {
        res.json(getDesktopContent());
    }
});
浏览器缓存条目的创建
// 浏览器会为不同的Vary值创建不同的缓存条目
const cacheEntries = [
    {
        url: 'https://example.com/api/data',
        vary: {
            'Accept-Encoding': 'gzip, deflate',
            'User-Agent': 'Chrome/118.0.0.0'
        },
        response: '/* gzip压缩的Chrome版本 */'
    },
    {
        url: 'https://example.com/api/data',
        vary: {
            'Accept-Encoding': 'br, gzip',
            'User-Agent': 'Firefox/119.0'
        },
        response: '/* brotli压缩的Firefox版本 */'
    },
    {
        url: 'https://example.com/api/data',
        vary: {
            'Accept-Encoding': 'gzip, deflate',
            'User-Agent': 'Safari/17.0'
        },
        response: '/* gzip压缩的Safari版本 */'
    }
];
Vary头的注意事项
// ❌ 过度使用Vary会导致缓存效率低下
app.get('/api/data', (req, res) => {
    res.set({
        'Cache-Control': 'public, max-age=3600',
        // 太多的Vary头会创建过多缓存条目,降低命中率
        'Vary': 'Accept-Encoding, User-Agent, Accept-Language, Cookie, Referer'
    });
});

// ✅ 合理使用Vary,只包含真正影响响应内容的头
app.get('/api/data', (req, res) => {
    res.set({
        'Cache-Control': 'public, max-age=3600',
        'Vary': 'Accept-Encoding'  // 只根据压缩格式变化
    });
});

5.5 缓存的物理存储

Chrome缓存位置
# Windows
C:\Users\{username}\AppData\Local\Google\Chrome\User Data\Default\Cache

# macOS
~/Library/Caches/Google/Chrome/Default/Cache

# Linux
~/.cache/google-chrome/Default/Cache
缓存文件结构
Cache/
├── index              # 缓存索引文件
├── data_0             # 缓存数据文件块
├── data_1
├── data_2
├── data_3
└── f_000001           # 具体的缓存文件

6. 私有缓存机制详解

6.1 私有缓存 vs 公共缓存

私有缓存(Private Cache)是HTTP缓存机制中的重要概念,它确保用户个人数据的安全性和隐私性。

基本概念对比
# 私有缓存 - 只在用户浏览器中存储
Cache-Control: private, max-age=300

# 公共缓存 - 可在代理服务器、CDN等共享缓存中存储
Cache-Control: public, max-age=3600

# 默认情况(通常被视为私有)
Cache-Control: max-age=1800
核心区别分析
特性 私有缓存 (Private) 公共缓存 (Public)
存储位置 仅用户浏览器 浏览器 + 代理服务器 + CDN
共享性 单用户独享 多用户共享
适用场景 用户个人数据、敏感信息 静态资源、公共数据
安全性 高(不会泄露给其他用户) 低(可能被其他用户访问)
缓存效率 较低(每用户独立缓存) 高(多用户共享缓存)

6.2 私有缓存的工作机制

缓存存储位置限制
// 私有缓存只存储在用户浏览器中
const privateCacheLocations = {
    // ✅ 允许存储的位置
    browser: {
        memoryCache: '浏览器内存缓存',
        diskCache: '浏览器磁盘缓存',
        serviceWorker: 'Service Worker缓存'
    },

    // ❌ 不会存储在这些地方
    notStored: {
        proxyServer: '代理服务器缓存',
        cdn: 'CDN节点缓存',
        sharedCache: '共享缓存服务器',
        gatewayCache: '网关缓存'
    }
};
典型的私有缓存应用场景
// Express.js 示例 - 用户个人数据
app.get('/api/user/profile', authenticateUser, (req, res) => {
    const userProfile = getUserProfile(req.user.id);

    res.set({
        'Cache-Control': 'private, max-age=300', // 5分钟私有缓存
        'Vary': 'Authorization',  // 根据用户身份变化
        'ETag': generateETag(userProfile, req.user.id)
    });

    res.json(userProfile);
});

// 用户订单数据
app.get('/api/user/orders', authenticateUser, (req, res) => {
    const orders = getUserOrders(req.user.id);

    res.set({
        'Cache-Control': 'private, max-age=60', // 1分钟私有缓存
        'Vary': 'Authorization, Cookie'
    });

    res.json(orders);
});

// 购物车数据
app.get('/api/cart', authenticateUser, (req, res) => {
    const cartItems = getCartItems(req.user.id);

    res.set({
        'Cache-Control': 'private, max-age=120', // 2分钟私有缓存
        'Vary': 'Authorization'
    });

    res.json({
        items: cartItems,
        total: calculateTotal(cartItems),
        userId: req.user.id
    });
});

6.3 私有缓存的安全考量

防止敏感信息泄露
// ❌ 错误:敏感数据使用公共缓存
app.get('/api/user/bank-info', authenticateUser, (req, res) => {
    const bankInfo = getBankInfo(req.user.id);

    res.set({
        'Cache-Control': 'public, max-age=3600' // 危险!
    });

    res.json(bankInfo);
    // 问题:银行信息可能被代理服务器缓存,泄露给其他用户
});

// ✅ 正确:敏感数据使用私有缓存或禁用缓存
app.get('/api/user/bank-info', authenticateUser, (req, res) => {
    const bankInfo = getBankInfo(req.user.id);

    res.set({
        'Cache-Control': 'private, max-age=0', // 或者使用 'no-store'
        'Vary': 'Authorization'
    });

    res.json(bankInfo);
    // 确保敏感信息只在用户浏览器中短暂存储
});
基于用户身份的缓存隔离
// 使用 Vary 头确保不同用户的数据不会混淆
app.get('/api/user/dashboard', authenticateUser, (req, res) => {
    const dashboardData = getDashboardData(req.user.id);

    res.set({
        'Cache-Control': 'private, max-age=300',
        'Vary': 'Authorization, Cookie', // 关键:基于认证信息变化
        'ETag': generateETag(dashboardData, req.user.id)
    });

    res.json(dashboardData);
});

// 个性化推荐内容
app.get('/api/recommendations', authenticateUser, (req, res) => {
    const recommendations = getPersonalizedRecommendations(req.user);

    res.set({
        'Cache-Control': 'private, max-age=600', // 10分钟
        'Vary': 'Authorization, User-Agent'
    });

    res.json({
        recommendations,
        userId: req.user.id,
        generatedAt: new Date().toISOString()
    });
});

6.4 代理服务器和CDN对私有缓存的处理

代理服务器的处理逻辑
// 代理服务器的处理逻辑(简化示例)
function handleCacheControl(request, response) {
    const cacheControl = response.headers['cache-control'];

    if (cacheControl.includes('private')) {
        // 私有缓存:不在代理服务器缓存,直接转发给客户端
        console.log('Private cache detected, bypassing proxy cache');
        return forwardToClient(response);
    } else if (cacheControl.includes('public')) {
        // 公共缓存:可以在代理服务器缓存
        console.log('Public cache detected, storing in proxy cache');
        proxyCache.store(request, response);
        return forwardToClient(response);
    } else {
        // 默认处理(通常视为私有)
        return forwardToClient(response);
    }
}
CDN对私有缓存的配置
# Nginx CDN配置示例
location /api/user/ {
    proxy_pass http://backend;

    # 检查响应头中的 Cache-Control
    map $upstream_http_cache_control $no_cache {
        ~*private 1;
        default 0;
    }

    # 如果是 private,不在 CDN 缓存
    proxy_no_cache $no_cache;
    proxy_cache_bypass $no_cache;

    # 添加调试头
    add_header X-Cache-Status $upstream_cache_status;
}

# 静态资源可以使用公共缓存
location /static/ {
    proxy_pass http://backend;
    proxy_cache static_cache;
    proxy_cache_valid 200 1d;
}

6.5 私有缓存的最佳实践

缓存时间策略
const privateCacheStrategies = {
    // 用户基本信息 - 较长缓存(变化不频繁)
    userProfile: {
        cacheControl: 'private, max-age=1800',  // 30分钟
        vary: 'Authorization',
        useCase: '用户姓名、邮箱等基本信息'
    },

    // 用户状态数据 - 中等缓存
    userStatus: {
        cacheControl: 'private, max-age=300',   // 5分钟
        vary: 'Authorization',
        useCase: '在线状态、积分余额等'
    },

    // 实时用户数据 - 短期缓存
    userNotifications: {
        cacheControl: 'private, max-age=60',    // 1分钟
        vary: 'Authorization',
        useCase: '通知消息、实时更新'
    },

    // 敏感数据 - 协商缓存
    sensitiveData: {
        cacheControl: 'private, no-cache',      // 总是验证
        vary: 'Authorization',
        useCase: '支付信息、密码相关'
    },

    // 极敏感数据 - 完全禁用
    criticalData: {
        cacheControl: 'no-store',               // 不存储
        useCase: '银行卡号、身份证号'
    }
};
结合ETag的私有缓存
app.get('/api/user/settings', authenticateUser, (req, res) => {
    const settings = getUserSettings(req.user.id);
    const etag = generateETag(settings, req.user.id);

    // 检查客户端ETag(协商缓存)
    if (req.headers['if-none-match'] === etag) {
        return res.status(304).end();
    }

    res.set({
        'Cache-Control': 'private, max-age=600', // 10分钟强缓存
        'ETag': etag,
        'Vary': 'Authorization'
    });

    res.json(settings);
});

6.6 Service Worker中的私有缓存实现

// sw.js - Service Worker中实现私有缓存
self.addEventListener('fetch', event => {
    const url = new URL(event.request.url);

    // 用户相关的API使用私有缓存策略
    if (url.pathname.startsWith('/api/user/')) {
        event.respondWith(handlePrivateAPI(event.request));
    }
});

async function handlePrivateAPI(request) {
    // 获取用户标识(从认证token中提取)
    const userId = await getUserIdFromRequest(request);
    const cacheKey = `private-${userId}-${request.url}`;

    const cache = await caches.open('private-cache');

    // 检查私有缓存
    const cachedResponse = await cache.match(cacheKey);
    if (cachedResponse && !isExpired(cachedResponse)) {
        console.log('Private cache hit:', request.url);
        return cachedResponse;
    }

    // 网络请求
    try {
        const response = await fetch(request);

        // 只缓存成功的私有响应
        if (response.ok &&
            response.headers.get('cache-control')?.includes('private')) {

            // 添加过期时间标记
            const responseToCache = new Response(response.body, {
                status: response.status,
                statusText: response.statusText,
                headers: {
                    ...response.headers,
                    'sw-cached-at': Date.now().toString()
                }
            });

            await cache.put(cacheKey, responseToCache);
            console.log('Cached private response:', request.url);
        }

        return response;
    } catch (error) {
        // 网络失败时尝试返回过期的缓存
        if (cachedResponse) {
            console.log('Network failed, returning stale cache:', request.url);
            return cachedResponse;
        }
        throw error;
    }
}

// 辅助函数
async function getUserIdFromRequest(request) {
    const authHeader = request.headers.get('Authorization');
    if (authHeader) {
        // 从JWT token中提取用户ID(简化示例)
        const token = authHeader.replace('Bearer ', '');
        const payload = JSON.parse(atob(token.split('.')[1]));
        return payload.userId;
    }
    return 'anonymous';
}

function isExpired(response) {
    const cachedAt = response.headers.get('sw-cached-at');
    const maxAge = parseCacheControl(response.headers.get('cache-control'))['max-age'];

    if (cachedAt && maxAge) {
        const age = (Date.now() - parseInt(cachedAt)) / 1000;
        return age > maxAge;
    }

    return false;
}

7. HTML文件命名策略的深度思考

6.1 HTML版本化的理论可行性

传统方式:

https://example.com/index.html

版本化方式:

https://example.com/index.abc123.html
https://example.com/index-v1.2.3.html

6.2 HTML版本化面临的挑战

1. 入口访问问题
// 用户如何知道当前版本的HTML文件名?
const challenges = {
    directAccess: 'https://example.com/',           // 用户直接访问
    seoIndexing: '搜索引擎收录固定URL',              // SEO需求
    bookmarks: '用户书签保存',                      // 用户体验
    sharing: '分享链接需要稳定URL'                  // 社交分享
};
2. 服务器路由复杂性
// 需要额外的路由逻辑
app.get('/', (req, res) => {
    // 方案1:重定向到版本化文件
    const currentVersion = getCurrentVersion();
    res.redirect(`/index.${currentVersion}.html`);
});

// 方案2:动态服务内容
app.get('/', (req, res) => {
    const htmlContent = getVersionedHTML();
    res.set({
        'Cache-Control': 'public, max-age=31536000',
        'ETag': generateETag(htmlContent)
    });
    res.send(htmlContent);
});
3. SEO和用户体验问题
  • URL不稳定,影响搜索引擎排名
  • 用户无法直接访问固定地址
  • 需要额外的重定向逻辑,增加延迟
  • 分析工具难以跟踪页面访问

6.3 HTML版本化的实际应用场景

虽然主流做法是保持HTML文件名固定,但确实存在一些特殊场景:

1. 微前端架构
# 主应用
main-app/
├── index.html                    # 主入口(固定名称)
└── micro-apps/
    ├── user-module.a1b2c3.html   # 子应用HTML(版本化)
    ├── order-module.d4e5f6.html  # 子应用HTML(版本化)
    └── assets/...
// 动态加载子应用
const loadMicroApp = async (moduleName, version) => {
    const htmlUrl = `/micro-apps/${moduleName}.${version}.html`;
    const response = await fetch(htmlUrl);
    return response.text();
};
2. 多页面应用的非入口页面
# 电商网站示例
dist/
├── index.html                    # 首页(固定名称)
├── product.a1b2c3.html          # 商品页(可能版本化)
├── cart.d4e5f6.html             # 购物车页(可能版本化)
└── user.g7h8i9.html             # 用户中心(可能版本化)
3. A/B测试或灰度发布
# 不同版本的页面
dist/
├── index.html                    # 默认版本
├── index.experiment-a.html       # A版本
├── index.experiment-b.html       # B版本
└── index.canary.abc123.html      # 金丝雀版本
// 根据用户特征分发不同版本
app.get('/', (req, res) => {
    const userGroup = getUserGroup(req.user);
    const htmlFile = getHtmlForGroup(userGroup);
    res.sendFile(htmlFile);
});
4. CDN分发的组件库
# 组件库的HTML模板
cdn/
├── button.v1.2.3.html
├── modal.v2.1.0.html
└── table.v1.5.2.html

6.4 可能看到"乱码"HTML文件名的情况

1. 开发环境的热更新文件
# Webpack Dev Server临时文件
.tmp/
├── index.hot-update.a1b2c3.html
├── main.hot-update.d4e5f6.js
└── vendors.hot-update.e7f8g9.js
2. 构建工具的中间产物
# 构建过程中的临时文件
.cache/
├── chunk.abc123.html
├── vendor.def456.html
└── runtime.ghi789.html
3. 错误的构建配置
// 可能导致HTML文件名哈希化的错误配置
module.exports = {
    plugins: [
        new HtmlWebpackPlugin({
            filename: '[name].[contenthash].html', // ❌ 不推荐
            template: 'src/index.html'
        })
    ]
};

// 正确的配置
module.exports = {
    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html', // ✅ 推荐
            template: 'src/index.html'
        })
    ]
};

8. 最佳实践与架构建议

7.1 分层缓存策略

// Express.js 示例
const express = require('express');
const app = express();

// 1. HTML文件 - 协商缓存
app.get('*.html', (req, res, next) => {
    res.set({
        'Cache-Control': 'no-cache',
        'ETag': generateETag(req.path)
    });
    next();
});

// 2. 静态资源 - 强缓存
app.get('/static/*', (req, res, next) => {
    res.set({
        'Cache-Control': 'public, max-age=31536000, immutable'
    });
    next();
});

// 3. API接口 - 根据业务需求
app.get('/api/*', (req, res, next) => {
    if (req.path.includes('/user/')) {
        // 用户相关数据 - 私有缓存
        res.set({
            'Cache-Control': 'private, max-age=300'
        });
    } else if (req.path.includes('/public/')) {
        // 公共数据 - 较长缓存
        res.set({
            'Cache-Control': 'public, max-age=1800'
        });
    } else {
        // 默认 - 短期缓存
        res.set({
            'Cache-Control': 'private, max-age=60'
        });
    }
    next();
});

7.2 Nginx配置示例

server {
    listen 80;
    server_name example.com;
    root /var/www/html;

    # 默认首页
    location / {
        try_files $uri $uri/ /index.html;
    }

    # HTML文件 - 协商缓存
    location ~* \.html$ {
        add_header Cache-Control "no-cache";
        add_header ETag $upstream_http_etag;

        # 安全头
        add_header X-Frame-Options "SAMEORIGIN";
        add_header X-Content-Type-Options "nosniff";
    }

    # 静态资源 - 强缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        add_header Cache-Control "public, max-age=31536000, immutable";

        # 启用压缩
        gzip_static on;

        # 跨域支持(如果需要)
        add_header Access-Control-Allow-Origin "*";
    }

    # API接口 - 灵活配置
    location /api/ {
        proxy_pass http://backend;

        # 根据路径设置不同的缓存策略
        location /api/static/ {
            add_header Cache-Control "public, max-age=3600";
        }

        location /api/user/ {
            add_header Cache-Control "private, max-age=300";
        }
    }
}

7.3 Service Worker增强缓存控制

// sw.js - Service Worker缓存策略
const CACHE_NAME = 'app-cache-v1';
const STATIC_CACHE = 'static-cache-v1';

self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(STATIC_CACHE).then(cache => {
            return cache.addAll([
                '/css/app.css',
                '/js/app.js',
                '/images/logo.png'
            ]);
        })
    );
});

self.addEventListener('fetch', event => {
    const { request } = event;
    const url = new URL(request.url);

    // HTML文件 - 网络优先策略
    if (request.destination === 'document') {
        event.respondWith(networkFirst(request));
    }
    // 静态资源 - 缓存优先策略
    else if (url.pathname.startsWith('/static/')) {
        event.respondWith(cacheFirst(request));
    }
    // API请求 - 网络优先,短期缓存
    else if (url.pathname.startsWith('/api/')) {
        event.respondWith(networkFirst(request, 300)); // 5分钟缓存
    }
});

// 网络优先策略
async function networkFirst(request, maxAge = 0) {
    try {
        const response = await fetch(request);

        if (response.ok && maxAge > 0) {
            const cache = await caches.open(CACHE_NAME);
            cache.put(request, response.clone());
        }

        return response;
    } catch (error) {
        const cachedResponse = await caches.match(request);
        if (cachedResponse) {
            return cachedResponse;
        }
        throw error;
    }
}

// 缓存优先策略
async function cacheFirst(request) {
    const cachedResponse = await caches.match(request);
    if (cachedResponse) {
        return cachedResponse;
    }

    const response = await fetch(request);
    if (response.ok) {
        const cache = await caches.open(STATIC_CACHE);
        cache.put(request, response.clone());
    }

    return response;
}

7.4 构建工具配置最佳实践

Webpack配置
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    output: {
        filename: 'js/[name].[contenthash].js',
        chunkFilename: 'js/[name].[contenthash].chunk.js',
        assetModuleFilename: 'assets/[name].[contenthash][ext]',
        clean: true
    },

    optimization: {
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    chunks: 'all',
                }
            }
        }
    },

    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html', // ✅ 固定名称
            template: 'src/index.html',
            inject: true,
            minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeRedundantAttributes: true
            }
        }),

        new MiniCssExtractPlugin({
            filename: 'css/[name].[contenthash].css'
        })
    ]
};
Vite配置
// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
    build: {
        rollupOptions: {
            output: {
                entryFileNames: 'js/[name].[hash].js',
                chunkFileNames: 'js/[name].[hash].js',
                assetFileNames: (assetInfo) => {
                    const extType = assetInfo.name.split('.').pop();
                    if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) {
                        return `images/[name].[hash][extname]`;
                    }
                    if (/css/i.test(extType)) {
                        return `css/[name].[hash][extname]`;
                    }
                    return `assets/[name].[hash][extname]`;
                }
            }
        }
    }
});

7.5 缓存性能监控

// 缓存性能监控
class CacheMonitor {
    constructor() {
        this.metrics = {
            hits: 0,
            misses: 0,
            networkRequests: 0,
            cacheSize: 0
        };
    }

    recordCacheHit(resource) {
        this.metrics.hits++;
        console.log(`Cache hit: ${resource}`);
    }

    recordCacheMiss(resource) {
        this.metrics.misses++;
        console.log(`Cache miss: ${resource}`);
    }

    recordNetworkRequest(resource) {
        this.metrics.networkRequests++;
        console.log(`Network request: ${resource}`);
    }

    getHitRate() {
        const total = this.metrics.hits + this.metrics.misses;
        return total > 0 ? (this.metrics.hits / total * 100).toFixed(2) : 0;
    }

    generateReport() {
        return {
            hitRate: `${this.getHitRate()}%`,
            totalRequests: this.metrics.hits + this.metrics.misses,
            networkRequests: this.metrics.networkRequests,
            cacheEfficiency: this.metrics.hits / this.metrics.networkRequests
        };
    }
}

// 使用示例
const monitor = new CacheMonitor();

// 在Service Worker中使用
self.addEventListener('fetch', event => {
    const request = event.request;

    event.respondWith(
        caches.match(request).then(response => {
            if (response) {
                monitor.recordCacheHit(request.url);
                return response;
            }

            monitor.recordCacheMiss(request.url);
            monitor.recordNetworkRequest(request.url);

            return fetch(request);
        })
    );
});

8.6 缓存策略决策矩阵

资源类型 变化频率 实时性要求 推荐策略 Cache-Control 备注
HTML入口文件 中等 协商缓存 no-cache + ETag 确保获取最新资源引用
CSS/JS文件 强缓存 max-age=31536000, immutable 使用哈希命名
图片资源 强缓存 max-age=31536000 可使用哈希命名
公共API数据 短期公共缓存 public, max-age=300 根据业务调整
用户个人数据 私有短期缓存 private, max-age=60 + Vary 避免敏感信息泄露
用户敏感数据 极高 私有协商缓存 private, no-cache + Vary 银行信息、支付数据
实时数据 极高 极高 禁用缓存 no-store, no-cache 股价、聊天消息等

总结

HTTP缓存机制是现代Web性能优化的核心技术之一。通过深入理解ETag协商缓存、强缓存判断流程、以及不同资源的缓存策略选择,我们可以构建高效的缓存架构:

关键要点回顾

  1. 协商缓存必定产生网络请求,但能显著减少数据传输量
  2. 强缓存优先级高于协商缓存,能完全避免网络请求
  3. HTML文件使用协商缓存是经过实践验证的最佳选择
  4. 私有缓存保护用户隐私,确保个人数据不会被共享缓存泄露
  5. Vary头控制缓存变化,根据请求头差异创建不同缓存条目
  6. 缓存策略选择应基于内容变化频率、实时性要求、版本控制等因素
  7. 分层缓存策略能够平衡性能和实时性需求

实践建议

  • 对于静态资源,使用哈希命名 + 强缓存
  • 对于HTML文件,使用协商缓存确保实时性
  • 对于用户个人数据,使用私有缓存 + Vary头
  • 对于敏感信息,使用私有协商缓存或禁用缓存
  • 合理使用Vary头,避免过度细分缓存
  • 使用Service Worker增强缓存控制能力
  • 建立缓存性能监控机制

通过合理的缓存策略设计,我们能够在保证用户体验的同时,显著提升Web应用的性能表现。


网站公告

今日签到

点亮在社区的每一天
去签到