告别 200 OK:利用 HTTP Last-Modified/ETag 实现高效 304 缓存

2025-12-01

在 HTTP 协议中,Last-Modified 是一个非常重要的响应头(Response Header)。

它的作用是

指示资源上次修改的时间服务器(Server)通过这个 Header 告诉浏览器或其他客户端(Client),它所请求的这个资源(比如 HTML 文件、图片、CSS 文件等)是在什么时候最后一次被修改的。

实现缓存控制客户端在收到这个 Header 后,会把这个时间值和资源一起缓存起来。下次请求同一个资源时,客户端就会发送一个相关的请求头 If-Modified-Since,带着上次记录的 Last-Modified 时间,询问服务器资源是否已经更新。

如果资源没有更新,服务器会返回一个 304 Not Modified 状态码,并且不发送资源内容,这样可以大大节省带宽和加载时间。

如果资源已经更新,服务器会正常返回 200 OK 状态码和新的资源内容,同时还会附带新的 Last-Modified 时间。

尽管 Last-Modified 很实用,但在实际编程中,尤其是在处理动态内容或部署环境时,可能会遇到一些问题。

Last-Modified 最适合用于静态文件(如图片、纯 CSS/JS 文件)。如果你的网页是通过程序动态生成的(比如从数据库获取数据后渲染),那么每次访问,即使数据没有变化,页面内容也可能被视为“新”的,导致服务器每次都发送 200 OK,无法有效利用 Last-Modified 缓存。

对于动态内容,更好的缓存控制机制是使用 ETag(Entity Tag)。ETag 是服务器为资源分配的一个“指纹”或版本标识符。

工作原理服务器计算资源的内容哈希值(Hash)作为 ETag 值,并在响应头中发送。客户端缓存 ETag。下次请求时,客户端发送 If-None-Match 头。

优点即使资源生成时间没变,但内容有微小变化,ETag 也会改变,保证了缓存的准确性。反之,内容没变,ETag 不变,可以有效返回 304。

在 Python 的 Web 框架中,很多内置了 ETag 支持,或者你可以手动计算内容的哈希值。

from flask import Flask, request, make_response

import hashlib

app = Flask(__name__)

# 假设这是从数据库获取的动态内容

def get_dynamic_content():

# 这里的 data 可能是从数据库查询出来的结果

data = "

欢迎来到我的动态网页!

当前时间: 2025-12-01

"

return data

@app.route('/dynamic')

def dynamic_page():

content = get_dynamic_content()

# 1. 计算内容的哈希值作为 ETag

content_hash = hashlib.sha1(content.encode('utf-8')).hexdigest()

# 2. 检查客户端是否发送了 If-None-Match

if_none_match = request.headers.get('If-None-Match')

# 3. 如果客户端的 ETag 和当前的 ETag 一致,则返回 304

if if_none_match == content_hash:

response = make_response('', 304)

response.headers['ETag'] = content_hash

return response

# 4. 否则,正常返回内容和新的 ETag

response = make_response(content, 200)

response.headers['Content-Type'] = 'text/html; charset=utf-8'

response.headers['ETag'] = content_hash

# 通常也建议配合 Cache-Control: no-cache, public 来使用 ETag

# response.headers['Cache-Control'] = 'no-cache, public'

return response

# if __name__ == '__main__':

# app.run(debug=True)

Last-Modified 的时间格式必须是 RFC 1123 标准时间格式(例如Sat, 01 Dec 2025 12:00:00 GMT)。如果你的代码使用了本地时间格式,或者时间精度(例如毫秒级)不符合标准,可能会导致缓存验证失败。HTTP 头中的时间通常只精确到秒。

在任何编程语言中,确保你用来生成 Last-Modified 的时间都是格林威治标准时间 (GMT) 或 UTC,并且格式是标准的。

使用标准的库函数来格式化时间,以确保合规性。

const express = require('express');

const app = express();

// 模拟资源的最后修改时间,使用 UTC 时间对象

const lastModifiedTime = new Date('2025-12-01T10:30:00.000Z');

app.get('/static-data', (req, res) => {

// 1. 将 UTC 时间对象格式化为 HTTP 标准格式

// toUTCString() 是最安全、最常用的方法

const lastModifiedString = lastModifiedTime.toUTCString();

// 2. 检查 If-Modified-Since 请求头

const ifModifiedSince = req.header('if-modified-since');

// 3. 如果请求头存在,并且时间比服务器的 Last-Modified 要新或相等

if (ifModifiedSince) {

// 由于时间比较可能会有偏差,通常用库函数或直接字符串比较

// 这里的简化比较仅为演示目的

const clientTime = new Date(ifModifiedSince);

if (clientTime >= lastModifiedTime) {

// 资源未修改,返回 304

res.set('Last-Modified', lastModifiedString);

return res.sendStatus(304);

}

}

// 4. 正常返回内容和 Last-Modified 头

res.set('Last-Modified', lastModifiedString);

res.send('这是静态内容。');

});

// app.listen(3000, () => console.log('Server running on port 3000'));

缓存头适用于验证机制优先级Last-Modified静态文件(时间戳)If-Modified-Since较低(基于时间)ETag动态内容(内容哈希)If-None-Match较高(基于内容)最佳实践建议

对于静态资源,例如图片、CSS 和 JS 文件,让你的 Web 服务器(如 Nginx, Apache)自动处理 Last-Modified。它们在这方面做得非常高效和准确。

对于动态生成的页面或 API 响应,请优先使用 ETag 来进行缓存控制。

同时使用 Cache-Control 头可以提供更精细的控制,例如 Cache-Control: max-age=600, public (浏览器可以缓存 600 秒) 或 Cache-Control: no-cache (必须向服务器验证,但允许缓存)。