深入解析协商缓存(弱缓存)

核心机制:内容验证而非文件验证

协商缓存的核心是验证资源内容是否变化,而不是文件本身是否修改(例如文件元数据变化但内容未变时不应更新)

两种验证机制及优先级

  1. ETag/If-None-Match(优先级更高)

    • 工作原理

      • 服务器生成资源内容的哈希值(如 ETag: "33a64df55142f"

      • 浏览器后续请求携带 If-None-Match: "33a64df55142f"

      • 服务器比较哈希值:

        nginx

        csharp 复制代码
        # Nginx 自动启用 ETag(默认配置)
        etag on;
        
        # 验证逻辑伪代码
        if request.headers['If-None-Match'] == generate_etag(current_content) {
            return 304;
        } else {
            return 200 with new content;
        }
    • 优势

      • 精确感知内容变化(1字节变化也会使哈希值改变)
      • 避免时间同步问题
      • 处理文件重命名但内容不变的情况
  2. Last-Modified/If-Modified-Since

    • 工作原理

      • 服务器返回最后修改时间(如 Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT

      • 浏览器后续请求携带 If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT

      • 服务器比较修改时间:

        nginx

        bash 复制代码
        # 自动启用(默认)
        if_modified_since exact;  # 精确模式
        
        # 验证逻辑伪代码
        if request.headers['If-Modified-Since'] >= resource.modified_time {
            return 304;
        } else {
            return 200;
        }
    • 缺陷

      • 最小时间单位为秒,1秒内多次修改无法识别
      • 文件内容未变但修改时间更新(如打包重建)会误判
      • 服务器时间不同步导致问题

优先级规则

当两种机制同时存在时:

原因:ETag 基于内容哈希,比时间戳更可靠(符合 HTTP/1.1 规范 RFC 7232)

关键实践:内容变化检测的陷阱与解决方案

场景:内容未变但文件改变的案例

  1. 文件重建导致时间戳更新

    • 问题:CI/CD 流水线每次构建都更新文件时间戳

    • 解决方案:

      nginx

      csharp 复制代码
      # 禁用 Last-Modified,强制使用 ETag
      add_header Last-Modified "";
      etag on;
  2. ETag 误判问题

    • 问题:分布式服务器使用 inode 生成 ETag(不同节点的 inode 不同)

    • 解决方案(Nginx):

      nginx

      perl 复制代码
      # 关闭默认 ETag(包含 inode)
      etag off;
      
      # 自定义基于内容的 ETag
      add_header ETag "%X-Content-MD5";  # 需要安装 ngx_http_ssi_filter_module

高级内容验证策略

  1. 弱校验 ETag(W/ 前缀)

    • 适用场景:内容语义不变的小修改(如空格调整)
    • 服务器返回:ETag: W/"33a64df55142f"
    • 浏览器行为:接受弱 ETag 匹配的 304 响应
  2. 内容摘要验证

    nginx

    bash 复制代码
    # 生成 SHA-256 内容摘要
    add_header Digest 'sha-256=base64(sha256(content))';
    • 浏览器通过 Want-Digest 头主动请求验证

缓存配置最佳实践

强缓存 + 协商缓存组合策略

nginx

csharp 复制代码
server {
    # 带哈希的静态资源(强缓存)
    location ~* .[a-f0-9]{8}.(js|css)$ {
        add_header Cache-Control "public, max-age=31536000, immutable";
        etag off;  # 不需要协商缓存
    }
    
    # 无哈希资源(协商缓存)
    location ~* .(js|css)$ {
        add_header Cache-Control "public, max-age=0, must-revalidate";
        etag on;
    }
    
    # HTML 文件(禁用缓存)
    location /index.html {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        etag on;
    }
}

动态内容缓存验证

javascript

javascript 复制代码
// 前端主动验证内容更新
async function checkContentUpdate(url, storedETag) {
  const response = await fetch(url, {
    headers: { 'If-None-Match': storedETag },
    cache: 'no-cache'
  });
  
  if (response.status === 304) {
    console.log('内容未变化');
  } else {
    const newETag = response.headers.get('ETag');
    // 存储新 ETag 并更新内容
  }
}

验证工具与调试技巧

  1. 浏览器网络面板分析

    • 304 响应:协商缓存生效
    • Provisional headers:强缓存生效
  2. CURL 手动验证

    bash

    bash 复制代码
    # 首次获取 ETag
    curl -I https://example.com/app.js
    
    # 发送验证请求
    curl -H 'If-None-Match: "33a64df55142f"' -I https://example.com/app.js
  3. 关键头信息

    http

    arduino 复制代码
    HTTP/1.1 304 Not Modified
    ETag: "33a64df55142f"
    Cache-Control: max-age=0, must-revalidate

特殊场景处理

  1. Service Worker 的缓存验证

    javascript

    csharp 复制代码
    self.addEventListener('fetch', event => {
      event.respondWith(
        caches.match(event.request).then(cached => {
          return fetch(event.request).then(network => {
            // 比较 ETag
            if (cached && 
                cached.headers.get('ETag') === network.headers.get('ETag')) {
              return cached;
            }
            return network;
          }).catch(() => cached);
        })
      );
    });
  2. CDN 边缘计算验证

    js

    csharp 复制代码
    // Cloudflare Worker 示例
    addEventListener('fetch', event => {
      const request = event.request;
      if (request.headers.get('If-None-Match')) {
        // 直接比较 ETag 避免回源
        const etagMatch = /* 从 KV 存储获取 ETag */;
        if (etagMatch) return new Response(null, { status: 304 });
      }
      event.respondWith(handleRequest(request));
    });

通过精确控制 ETag 生成策略和缓存头配置,可在保证缓存效率的同时,实现真正基于内容变化的精准更新检测,避免不必要的资源重新加载。

相关推荐
素界UI设计1 小时前
开源网页生态掘金:从Bootstrap二次开发到行业专属组件库的技术变现
前端·开源·bootstrap
潘小安1 小时前
【译】六个开发高手使用的 css 动画秘诀
前端·css·性能优化
前端开发爱好者1 小时前
尤雨溪官宣:Vite 历史性的一刻!超越 Webpack!
前端·javascript·vite
前端开发爱好者1 小时前
Vue3 "抛弃" Axios !用上了 专属请求库!
前端·javascript·vue.js
前端开发爱好者1 小时前
"Lodash" 的终极版!Vue、React 通杀!
前端·javascript·全栈
前端开发爱好者1 小时前
TanStack:不止于 Vue!一个库,真·通杀所有框架!
前端·javascript·vue.js
curdcv_po1 小时前
Three.js,给纹理,设颜色空间
前端
站大爷IP1 小时前
HTTPS代理抓包完全攻略:工具、配置与高级技巧
前端
洛卡卡了1 小时前
“改个配置还要发版?”搞个配置后台不好吗
前端·后端·架构
林太白2 小时前
CommonJS和ES Modules篇
前端·面试