一文让你学会使用http缓存
http缓存
http缓存分为强缓存和协商缓存
强缓存: Cache-Control
、Expires
协商缓存:ETag
和 Last-Modified
。
http缓存策略的命中过程
下面是HTTP缓存策略的命中过程和优先级的一般流程:
- 浏览器发起请求: 当浏览器需要请求一个资源时,它会向服务器发送一个HTTP请求。
- 检查强制缓存: 浏览器首先会检查缓存是否有强制缓存副本。它会查看响应的缓存头字段,如
Cache-Control
和Expires
。如果资源在缓存有效期内(根据max-age
或Expires
字段),浏览器直接从缓存中获取资源,不会向服务器发起请求。 - 检查协商缓存: 如果资源不在强制缓存有效期内,浏览器会向服务器发送一个条件请求,带上缓存相关的标识,如
ETag
或If-Modified-Since
。服务器会检查这些标识,如果资源未发生改变,服务器返回一个状态码为304(Not Modified)的响应,告诉浏览器可以使用缓存副本。浏览器接收到304响应后,从缓存中获取资源。 - 获取完整的响应: 如果资源未命中强制缓存和协商缓存,或者缓存副本失效,服务器会返回一个完整的响应,包含新的资源内容。浏览器将接收到的响应存储在缓存中,并将其用于未来的请求。
Cache-contorl
以下是一些常见的 Cache-Control
指令值及其含义:
no-store
:禁止缓存请求和响应的任何部分,每次都要向服务器发送请求。no-cache
:必须进行协商缓存验证,缓存可以使用已经缓存的响应,但必须与服务器进行确认以确保它仍然有效。public
:响应可以被任何缓存(包括客户端和代理服务器)缓存。private
:响应只能被单个用户缓存,通常不会被共享缓存(如代理服务器)缓存。max-age=<seconds>
:指定资源在被认为过期之前可以被缓存的最长时间,单位为秒。
接下来用koa框架来展示如何使用cache-contorl,需要使用到两个文件index.html和server.js
index.html
<!DOCTYPE html>
<html>
<head>
<title>API 请求示例</title>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<h1>API 请求示例</h1>
<button onclick="fetchData()">获取数据</button>
<div id="response"></div>
<script>
function fetchData() {
axios.get('http:localhost:3000/api/hello')
.then(function (response) {
document.getElementById('response').innerText = response.data;
})
.catch(function (error) {
console.error(error);
});
}
</script>
</body>
</html>
server.js
const Koa = require('koa');
const Router = require('koa-router');
const cors = require('koa2-cors');
const app = new Koa();
const router = new Router();
// 允许跨域请求
app.use(cors());
// 定义接口路由及处理逻辑
router.get('/api/hello', async (ctx) => {
ctx.set('Cache-Control', 'max-age=3600');
ctx.body = 'Hello, API!';
});
// 将路由中间件注册到 Koa 应用中
app.use(router.routes()).use(router.allowedMethods());
// 启动服务
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
在上述示例中,通过 ctx.set()
方法设置了响应头中的 Cache-Control
字段为 'max-age=3600'
,表示资源在客户端的缓存有效期为 1 小时。这意味着浏览器在请求该资源后,会将其缓存并在接下来的 1 小时内直接使用缓存,而不会发起新的请求。
我们请求两次,就会发现第二次请求会出现from disk cache就表示命中缓存,从磁盘中返回存储的结果
Expires
Expires
的值是一个表示过期时间的日期字符串。这个日期字符串遵循 RFC 1123 格式
Expires: Wed, 01 Jun 2023 12:00:00 GMT
Expires
头字段的值是基于服务器的绝对时间,由于客户端和服务器之间的时间可能存在差异,因此更常见且推荐的是使用相对时间而不是绝对时间来控制缓存,例如使用 Cache-Control
头字段的 max-age
指令。
那么我们只需要把server.js改成下面的样子,为了和之前的区分我们把返回改成Hello, Expires!
const Koa = require('koa');
const Router = require('koa-router');
const cors = require('koa2-cors');
const app = new Koa();
const router = new Router();
// 允许跨域请求
app.use(cors());
// router.get('/api/hello', async (ctx) => {
// ctx.set('Cache-Control', 'max-age=3600');
// ctx.body = 'Hello, API!';
// });
router.get('/api/hello', async (ctx) => {
// 设置过期时间为一天后
const expirationDate = new Date();
expirationDate.setDate(expirationDate.getDate() + 1);
// 将过期时间设置到 Expires 头字段
ctx.set('Expires', expirationDate.toUTCString());
ctx.body = 'Hello, Expires!';
});
// 将路由中间件注册到 Koa 应用中
app.use(router.routes()).use(router.allowedMethods());
// 启动服务
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
但是我们发现,当我们请求返回的还是Hello, API!,那是因为我们之前把cache-control设置了1小时,所以现在还是从缓存中拿到结果,并没有真正的请求服务器,
这时候我们只需要右击选择清空缓存并硬性重新加载,就可以把之前的缓存去掉了。
这时候再请求,发现返回的是Hello, Expires!,并且在第二次请求中会直接从缓存中返回结果
Cache-contorl与Expires区别
-
Cache-Control
是一个通用的缓存控制指令,可以在响应头中设置。它提供了更细粒度和灵活的缓存控制选项,例如指定缓存的最大有效时间、禁用缓存、要求验证等。 -
Expires
是一个指定资源过期时间的字段,以绝对时间的形式表示。服务器通过在响应头中设置Expires
字段,告诉客户端在指定的时间之后,应该认为该资源已经过期。 -
如果同时设置了
Cache-Control
和Expires
,Cache-Control
的优先级更高,会覆盖Expires
的设置。在现代的 Web 开发中,Cache-Control
更常用,因为它提供了更好的灵活性和可靠性。
ETag
ETag
的取值可以是任意字符串,通常由服务器生成并与资源相关联。常见的取值方式有以下几种:
- 基于内容的哈希值:服务器可以使用哈希算法(如 MD5 或 SHA)对资源内容进行计算,生成唯一的哈希值作为
ETag
的取值。例如,ETag: "d41d8cd98f00b204e9800998ecf8427e"
。 - 版本号:如果资源有版本控制机制,服务器可以将资源的版本号作为
ETag
的取值。例如,ETag: "v1.2.3"
。 - 时间戳:服务器可以使用资源的最后修改时间或其他时间信息作为
ETag
的取值。例如,ETag: "1622781692"
。
客户端在发送条件请求时,可以使用 If-None-Match
请求头字段将之前获取的 ETag
值发送给服务器,服务器会根据这个值来判断资源是否发生了变化。如果 ETag
值匹配,则服务器返回 304 Not Modified 状态码,表示客户端的缓存仍然有效,可以继续使用缓存的资源。如果 ETag
值不匹配,服务器返回新的资源并带有新的 ETag
值。
我们基于server.js的基础上添加一个新的接口
const etag = require('etag');
router.get('/api/etag', async (ctx) => {
// 生成资源内容
const resourceContent = 'hello Etag';
// 生成 ETag
const resourceETag = etag(resourceContent);
// 设置 ETag 到响应头
ctx.set('ETag', resourceETag);
// 检查客户端发送的 If-None-Match 请求头
const clientETag = ctx.get('If-None-Match');
if (clientETag === resourceETag) {
// ETag 匹配,返回 304 Not Modified
ctx.status = 304;
} else {
// ETag 不匹配,返回新的资源内容
ctx.body = resourceContent;
}
});
在处理请求时,我们首先从请求头中获取客户端发送的 If-None-Match
值,然后将其与服务器生成的 ETag
值进行比较。如果两者匹配,表示客户端的缓存仍然有效,服务器返回 304 Not Modified 状态码。如果不匹配,表示客户端的缓存已过期或不存在,服务器返回新的资源内容。
Last-Modified
Last-Modified
的值是一个表示日期和时间的字符串,格式为 HTTP-date(例如:Wed, 21 Oct 2015 07:28:00 GMT)。
Last-Modified
响应头通常与条件请求一起使用,以便客户端在下次请求资源时与服务器验证资源是否发生了修改。客户端可以将之前获取到的 Last-Modified
值通过请求头的 If-Modified-Since
字段发送给服务器,服务器将根据该值来判断资源是否被修改过。
我们基于server.js的基础上添加一个新的接口
let resource = {
id: 1,
content: 'hello lastModified',
lastModified: new Date().toUTCString()
};
router.get('/api/last-modified',async (ctx) => {
const ifModifiedSince = ctx.get('if-modified-since');
if (ifModifiedSince && new Date(ifModifiedSince) >= new Date(resource.lastModified)) {
// 如果资源未被修改,返回 304 Not Modified 状态码
ctx.status = 304;
} else {
// 如果资源已被修改或是首次请求,返回新的响应
ctx.set('Last-Modified', resource.lastModified);
ctx.body = resource.content
}
});
服务器会检查请求头中的 If-Modified-Since
字段的值。如果这个时间戳大于或等于资源的最后修改时间,服务器会返回 304 Not Modified 状态码,表示资源未被修改,客户端可以使用缓存的响应。否则,服务器会返回新的响应,同时在响应头中设置 Last-Modified
字段,表示资源的最后修改时间。
Etag与Last-Modified的区别
ETag
是一个由服务器生成的唯一标识符,而Last-Modified
是表示资源最后修改时间的时间戳。- ETag 更精确,能够准确识别资源的变化,不仅仅依赖于最后修改时间。
Last-Modified
只能以秒为单位进行比较,而ETag
没有这个限制。- 在某些情况下,由于文件系统时间戳的精度问题,
Last-Modified
可能不够可靠。 ETag
可以在服务器生成响应时进行更复杂的计算,例如使用哈希算法,以确保唯一性。- 优先级方面,
ETag
的优先级高于Last-Modified
。当客户端发送了If-None-Match
头字段时,服务器会优先比较ETag
值,如果匹配,则返回 304 Not Modified 状态码。只有在ETag
值不匹配的情况下,服务器才会继续比较Last-Modified
值。 在实际应用中,通常会同时使用 ETag 和 Last-Modified 进行协商缓存,以充分利用它们各自的优势,并提高缓存的精确性和效率。
转载自:https://juejin.cn/post/7240740177450205239