1.1 ETag 是什么
ETag(Entity Tag)是万维网协议 HTTP 的一部分。它是 HTTP 协议提供的若干机制中的一种 Web 缓存验证机制,并且允许客户端进行缓存协商。这使得缓存变得更加高效,而且节省带宽。如果资源的内容没有发生改变,Web 服务器就不需要发送一个完整的响应。
1.2 ETag 的作用
ETag 是一个不透明的标识符,由 Web 服务器根据 URL 上的资源的特定版本而指定。如果 URL 上的资源内容改变,一个新的不一样的 ETag 就会被生成。ETag 可以看成是资源的指纹,它们能够被快速地比较,以确定两个版本的资源是否相同。
需要注意的是 ETag 的比较只对同一个 URL 有意义 —— 不同 URL 上资源的 ETag 值可能相同也可能不同。
1.3 ETag 的语法
ETag: W/“<etag_value>”
ETag: “<etag_value>”
W/(可选):‘W/’(大小写敏感) 表示使用弱验证器。弱验证器很容易生成,但不利于比较。强验证器是比较的理想选择,但很难有效地生成。相同资源的两个弱 Etag 值可能语义等同,但不是每个字节都相同。
“<etag_value>”:实体标签唯一地表示所请求的资源。它们是位于双引号之间的 ASCII 字符串(如 “2c-1799c10ab70” )。没有明确指定生成 ETag 值的方法。通常是使用内容的散列、最后修改时间戳的哈希值或简单地使用版本号。比如,MDN 使用 wiki 内容的十六进制数字的哈希值。
1.4 ETag 的使用
在大多数场景下,当一个 URL 被请求,Web 服务器会返回资源和其相应的 ETag 值,它会被放置在 HTTP 响应头的 ETag 字段中:
HTTP/1.1 200 OK
Content-Length: 44
Cache-Control: max-age=10
Content-Type: application/javascript; charset=utf-8
ETag: W/“2c-1799c10ab70”
然后,客户端可以决定是否缓存这个资源和它的 ETag。以后,如果客户端想再次请求相同的 URL,将会发送一个包含已保存的 ETag 和 If-None-Match 字段的请求。
GET /index.js HTTP/1.1
Host: localhost:3000
Connection: keep-alive
If-None-Match: W/“2c-1799c10ab70”
客户端请求之后,服务器可能会比较客户端的 ETag 和当前版本资源的 ETag。如果 ETag 值匹配,这就意味着资源没有改变,服务器便会发送回一个极短的响应,包含 HTTP “304 未修改” 的状态。304 状态码告诉客户端,它的缓存版本是最新的,可以直接使用它。
HTTP/1.1 304 Not Modified
Cache-Control: max-age=10
ETag: W/“2c-1799c10ab70”
Connection: keep-alive
二、ETag 实战
2.1 创建 Koa 服务器
了解完 ETag 相关知识后,基于 koa、koa-conditional-get、koa-etag 和 koa-static 这些库来介绍一下,在实际项目中如何利用 ETag 响应头和 If-None-Match 请求头实现资源的缓存控制。
// server.js
const Koa = require(“koa”);
const path = require(“path”);
const serve = require(“koa-static”);
const etag = require(“koa-etag”);
const conditional = require(“koa-conditional-get”);
const app = new Koa();
app.use(conditional()); // 使用条件请求中间件
app.use(etag()); // 使用etag中间件
app.use( // 使用静态资源中间件
serve(path.join(__dirname, “/public”), {
maxage: 10 * 1000, // 设置缓存存储的最大周期,单位为秒
})
);
app.listen(3000, () => {
console.log(“app starting at port 3000”);
});
在以上代码中,使用了 koa-static 中间件来处理静态资源,这些资源被保存在 public 目录下。在该目录下,创建了 index.html 和 index.js 两个资源文件,文件中的内容分别如下所示:
2.1.1 public/index.html
ETag 使用示例
2.1.2 public/index.js
1
console.log(“大家好”);
在启动完服务器之后,打开 Chrome 开发者工具并切换到 Network 标签栏,然后在浏览器地址栏输入 http://localhost:3000/ 地址,接着多次访问该地址(地址栏多次回车)。下图是多次访问的结果:
2.2 ETag 和 If-None-Match
下面以 index.js 为例,来分析上图中与之对应的 HTTP 报文。对于 index.html 文件,感兴趣的小伙伴可以自行分析一下。接下来先来分析首次请求 index.js 文件的报文:
2.2.1 首次请求 — 请求报文
GET /index.js HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
…
2.2.2 首次请求 — 响应报文
HTTP/1.1 200 OK
Content-Length: 44
Cache-Control: max-age=10
ETag: W/“2c-1799c10ab70”
…
在使用了 koa-static 和 koa-etag 中间件之后,index.js 文件首次请求的响应报文中会包含 Cache-Control 和 ETag 的字段信息。
Cache-Control 描述的是一个相对时间,在进行缓存命中的时候,都是利用客户端时间进行判断,所以相比较 Expires,Cache-Control 的缓存管理更有效,安全一些。
2.2.3 10s内 — 请求报文
GET /index.js HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
…
2.2.4 10s内 — 响应信息(General)
Request URL: http://localhost:3000/index.js
Request Method: GET
Status Code: 200 OK (from memory cache)
Remote Address: [::1]:3000
Referrer Policy: strict-origin-when-cross-origin
2.2.5 10s内 — 响应信息(Response Headers)
Cache-Control: max-age=10
Connection: keep-alive
Content-Length: 44
ETag: W/“2c-1799c10ab70”
由于设置了 index.js 资源文件的最大缓存时间为 10s,所以在 10s 内浏览器会直接从缓存中读取文件的内容。需要注意的是,此时的状态码为:Status Code: 200 OK (from memory cache)。