前端缓存以及react-query(SWR)的实现
最近在阅读图解HTTP,看到了cache-control这块内容,之前的确关注很少。加上最近也正在使用react-query,也在疑惑他的缓存机制。刚好花时间研究了下前端缓存。
cache-control
-
max-age
首先http header是有cache-control这个属性, 当我们想使用缓存机制, 我们最常用的就是max-age。
例如我现在有一个请求a,请求a在http header设置的cache-control为max-age=10,那么浏览器就会根据这个请求a的url以及header和body等等作为你这个请求的唯一标识,存储在缓存中。当10s内你再次发送这个请求a,那么就会命中缓存,而不会去请求服务器。
此时,我是有一个疑问的,那么当请求a的max-age设置为10s,那么当我第5s的时候刷新浏览器,那么这个请求a是会取缓存还是会命中服务器取最新数据呢?
在刷新页面的情况下,浏览器默认会忽略缓存,并向后端服务器发送请求以获取最新的响应。这是因为刷新操作通常意味着用户希望获取最新的页面内容,而不是使用缓存的数据。
-
max-stale
我个人感觉max-stale使用场景不是很多,max-stale是处理缓存数据的,例如我设置了max-age=10 , max-stale=10,就代表发送一个请求后,我们这个缓存会存储10s,当缓存过期后,我们在过期后的10s内,我们还是会继续使用缓存。
-
no-cache && no-store
其中的no-cache并非是使浏览器不缓存文件,而是缓存文件却不使用缓存,强制浏览器向服务器获取资源。而no-store才是强制浏览器不缓存也不使用缓存。
no-cache也用于关闭强缓存
日常前端缓存实现
那么当我们知道了max-age和max-stale这些基础概念后,我们前端日常做缓存又是怎么做的呢?可能大家都知道强缓存/协商缓存的概念。
-
强缓存
强缓存其实就是我们上面所讲的这些知识了,通过浏览器来处理, 我前端认为这个请求缓存10s就缓存10s。服务器就算更新数据了,前端也会不知道的。
-
协商缓存
通用做法是后端会根据我们前端在请求头传给它某个标识去判断是否取缓存数据返回给前端。
http-header是提供了两个请求头标识,通用的两个请求头标识是If-Modified-Since / If-None-Match.
如果开启协商缓存,例如使用ETag, 当后端给ETag,set值后,过后的请求就会自动带上If-None-Match
image.png- If-Modified-Since
它的值是上一次请求中服务器返回的响应头中的"Last-Modified"字段的值,传给后端,后端会去比较,如果变化则会返回304,告诉前端使用缓存。 - If-None-Match
它的值是上一次请求中服务器返回的响应头中的"ETag"字段的值, 传给后端,后端会去比较,如果变化则会返回304,告诉前端使用缓存。
当这个请求,后端判断不需要从缓存中取的时候,会返回200和去查数据返回给前端,当后端判断需要从缓存中取的时候,后端会返回304 http 状态码
当http status code 为 304的时候,浏览器会默认去取缓存的值,而不需要开发者额外的操作
image.png
react-query / SWR
普通的缓存策略是这样的,也就是我们使用协商缓存:当一个资源的缓存过期之后,我们请求后端,后端需要花时间去查询返回给我们新的数据,在这个期间,客户端就得等待,直到请求结束。
而这个 SWR 策略是说:当资源过期,进行重新请求时,客户端可以不等待,直接使用过期的缓存,请求完成后缓存就更新了,下次用的就是新的了。
我一直理解为react-query或者swr是不是利用和http-header,现在想来react-query和swr其实也是强缓存的一种形式,只是通过代码层面,帮我们完成了缓存,重复请求等相关问题,也更好的帮助我们管理了后端数据。
以下是简单实现
const cache = new Map();
async function swr(cacheKey, fetcher, cacheTime) {
const data = cache.get(cacheKey) || { value: null, time: 0, promise: null };
cache.set(cacheKey, data);
const isStaled = Date.now() - data.time > cacheTime;
if (isStaled && !data.promise) {
data.promise = fetcher()
.then((val) => {
data.val = val;
data.time = Date.now();
})
.catch((err) => {
console.log(err);
})
.finally(() => {
data.promise = null;
});
}
if (data.promise && !data.value) await data.promise;
return data.value;
}
以下是模拟协商缓存demo
server.js
const http = require("http");
const fs = require("fs");
const server = http.createServer((req, res) => {
if (req.url === "/") {
fs.readFile("./index.html", (err, data) => {
if (err) {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("File not found");
} else {
res.writeHead(200, { "Content-Type": "text/html" });
res.end(data);
}
});
} else if (req.url === "/api/getUserInfo" && req.method === "GET") {
const user = { name: "John Doe", age: 30 };
res.setHeader("Cache-Control", "no-cache");
const ifNoneMatch = req.headers["if-none-match"];
if (ifNoneMatch === "liyu12") {
res.setHeader("ETag", "liyu12");
res.statusCode = 304;
res.end();
} else {
res.setHeader("ETag", "liyu12");
res.end(JSON.stringify(user));
}
}
});
server.listen(3333, () => {
console.log("server listen on 3333");
});
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<script>
fetch('/api/getUserInfo').then(user => console.log('user', user))
</script>
<body>
<div>Hello World</div>
</body>
</html>