浏览器Cache-Control ETag缓存机制
问题
- 基于镜像服务的响应结果拓展功能中的一个HTML页面,在GET URL固定时会自动执行document-ready事件。
- 经过进一步测试发现后续多次访问同一页面时其内容无变化。
- 查阅相关资料后发现这一现象的原因在于浏览器缓存机制。
缓存
»»» https://datatracker.ietf.org/doc/publication/rfc7232
»»» https://datatracker.ietf.org/doc/publication/rfc7234
»»» https://datatracker.ietf.org/doc/publication/rfc7232
»»» https://datatracker.ietf.org/doc/publication/rfc7234
浏览器缓存控制主要通过条件判断来识别哪些网络请求应路由至服务器端而哪些应使用本地缓存机制,并以此来减少带宽消耗并提升通信效率。
浏览器缓存控制涉及判断是否启用缓存以及判断缓存项是否过期。
浏览器缓存控制适用于处理那些不会动态变化或频繁更改的资源。
浏览器缓存控制是通过查询响应头中的特定字段来实现的。
缓存控制开关
- Pragma:HTTP/1.0中规定,
no-cache未指定缓存控制 - Cache-Control:http1.1定义,优先级高于Pargma,请求和响应中都可以包含,指令用于缓存开关和简单的有效时间
- Expires:响应头字段,当Cache-Control设置了max-age时Expires被忽略,GMT时间戳
- 打开新窗体,地址栏回车等的缓存行为可能不一样
缓存验证
max-age和Expires仅作为初步的有效期验证工具存在;它们通过初始请求中响应头信息中的200状态码字段能够获得更为详尽的有效期验证要求
ETag标识符:用于唯一标识资源的不同版本;其版本号相较于指定时间更为便捷地可辨识;便于迅速判别该资源是否存在更新或变更;不仅在缓存机制中具有重要作用,在支持部分响应查询方面也有独特价值
If-Match:该请求字段头部信息中包含匹配指定ETag值的资源标识符时才会触发响应(若使用If-None-Match则表示相反的含义)
当服务器确认请求中的资源在指定的时间之后尚未被修改时
If-Match优先级比If-Unmodified-Since高
Last-Modified:服务器上一次更新资源的时间戳(Last-Modified 标头:Tue, 15 Nov 1994 12:45:26 GMT),其精确度低于 ETag 标头(此标头可作为替代方案使用),可作为备用机制使用
demo
- 在第一次请求中服务端返回200状态码时,在headers字段中添加TeaG标记。
- 随后的请求中,在If-None-Match头字段和headers字段中插入TeaG标记。
- 服务端根据之前的TeaG标记与最新的TeaG标记进行比较后发现两者一致,则会发送304状态码;如果两者不一致,则会发送包含最新TeaG标记和201状态码的新响应。
<!--statuecode.html-->
<html>
<head>
<title>
状态码http response status code
</title>
<script src="http://code.jquery.com/jquery-1.8.3.js" type="text/javascript"></script>
</head>
<body>
<div>
<table>
<tr>
<td>正常状态</td>
<td>
当前网页是一个200状态
</td>
</tr>
<tr>
<td>cache控制</td>
<td>
<input type='button' value='Cache-Control age' onclick='test_cache("cache_age")' />
</td>
<td>
<input type='button' value='Cache-Control ETag' onclick='test_cache("cache_etag")' />
</td>
</tr>
</table>
<iframe class='cache' hidden>
</iframe>
</div>
<script type="text/javascript">
function test_cache(code) {
// debugger;
// ifram中的cache好像不管用,还是重新向服务端请求了
// document.querySelector("iframe.cache").src = code + ".html";
window.open(code + ".html")
}
</script>
</body>
</html>
<!--cache.html-->
<html>
<head>
<title>
304 Cache
</title>
<script src="http://code.jquery.com/jquery-1.8.3.js" type="text/javascript"></script>
</head>
<body>
<div>
{{word}}
</div>
</body>
</html>
from hashlib import md5
from werkzeug.http import generate_etag, is_resource_modified
from flask import Blueprint, request, make_response, render_template
blue_statue_code = Blueprint("blue_statue_code", __name__)
@blue_statue_code.route(rule="/")
def statue_code():
return make_response(render_template("statuecode.html"))
@blue_statue_code.route(rule="cache_age.html")
def statue_code_cache_age():
resp = make_response(render_template("cache.html", word="Age Test!"))
resp.cache_control.public = True
resp.cache_control.max_age = 31536000
return resp
etags, times = ["孙悟空", "孙悟空", "唐三藏"], 0
@blue_statue_code.route(rule="cache_etag.html")
def statue_code_cache_etag():
global times
resp = make_response(render_template("cache.html", word="Etag Test! " + etags[times % len(etags)]))
times += 1
resp.cache_control.public = True
# resp_etag = md5(resp.data).hexdigest()
# if "If-None-Match" in request.headers and request.headers["If-None-Match"] == resp_etag:
# return resp.data, 304
# else:
# resp.headers["ETag"] = resp_etag
# return resp
resp_etag = generate_etag(resp.data)
# is_resource_modified(request.environ, etag=resp_etag, data=resp.data)
if is_resource_modified(request.environ, data=resp.data):
resp.headers["ETag"] = resp_etag
return resp
else:
return resp.data, 304
tip
- 由于缓存的关键是以完整且包含GET参数的有效URL生成... (最后我通过添加t=时间戳解决了开头的问题)
- Flask默认采用WERKZEUG的DEBUGGED APPLICATION作为一种HTTP服务器,默认返回HTTP/1.0协议; 而浏览器中的ETag机制仅在HTTP/1.1及以上版本中被支持
from http.server import BaseHTTPRequestHandler
from flask import Flask, make_response, render_template, request, jsonify
from blue.statue_code import blue_statue_code
if __name__ == "__main__":
app = Flask(import_name="demo", template_folder="template", static_folder="static", static_url_path="/static")
app.register_blueprint(blue_statue_code, url_prefix="/statue_code")
print(app.url_map)
BaseHTTPRequestHandler.protocol_version = "HTTP/1.1"
app.run()
