前端里面的缓存,可以分为资源缓存
和数据缓存
,
资源缓存: 像html,js,css,img等资源没有更新的话就没必要再去发起请求,直接从缓存中读取,提高网页的整体加载速度;比如http缓存
数据缓存: 像cookie、localstorage、sessionStorage、indexedDB等方案将数据缓存在本地
Http缓存通常分为强制缓存和协商缓存,从第二次请求开始,主要区分就是几个响应头和请求头
测试代码
建一个文件夹,里面放上node代码和静态资源,起一个node服务
js
// main.js
const http = require("http");
const path = require("path");
const { stat, readFile } = require("fs/promises");
const { createHash } = require("crypto");
const port = 2024;
const server = http.createServer(async (req, res) => {
// 获取请求路径
const { pathname } = new URL(req.url, `http://localhost:${port}`);
// 获取文件绝对路径
let filePath = "";
if (pathname === "/") {
filePath = path.resolve(__dirname, "../static", `./index.html`);
} else {
filePath = path.resolve(__dirname, "../static", `.${pathname}`);
}
try {
// stat 读取文件,返回有关文件的信息对象
const stats = await stat(filePath);
if (stats.isFile()) { // 读取到文件
const fileRes = await readFile(filePath);
// 刷新看每次请求几个文件
console.log(filePath);
res.end(fileRes);
} else { // 读取到目录
res.statusCode = 404
res.end("<h1>NOT FOUND</h1>");
}
} catch {
// 读取报错
res.statusCode = 404
res.end("<h1>NOT FOUND</h1>");
}
});
server.listen(port, () => {
console.log("node服务启动");
});
强制缓存
浏览器在资源加载时,根据请求头中的 expires
和 cache-control
值来判断资源是否过期,未过期直接从本地缓存中读取资源,不需请求服务器;
强缓存对根文件html没用,下面代码演示,一般是针对js,css,img等资源
Expires
在 HTTP/1.0 中,有效期是通过 Expires 标头来指定的。
Expires
标头使用明确的时间而不是通过指定经过的时间来指定缓存的生命周期。
js
// main.js
// 强制缓存,设置明确时间,10秒后过期
res.setHeader("Expires", new Date(Date.now() + 1000 * 10).toUTCString());
注意:关掉Network中的Disable cache
,10秒内多次刷新,可以看到 css和js文件没有重新请求,但是index.html每次都请求了
缺点: expire
值是一个固定时间,如果在缓存期间修改了本地时间,客户端时间和服务端时间产生误差,会导致缓存失效
Cache-Control
HTTP1.1使用 Cathe-Control 响应头,使用max-age指定资源被缓存多久,而不是固定的时间点,解决了Expires的缺点。它有以下指令:
- max-age=xxx(单位秒): 设置缓存存储的最大周期,超过这个时间缓存被认为过期 (单位秒)。与
Expires
相反,时间是相对于请求的时间。 - public: 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容。
- private: 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容,比如:对应用户的本地浏览器。
- no-cache: 先把缓存内容提交给服务器进行验证 (协商缓存验证Etag),没有更新,可以使用缓存,否则重新加载
- no-store: 不使用任何缓存
js
// main.js
// 设置10秒过期
// res.setHeader("Expires", new Date(Date.now() + 1000 * 10).toUTCString());
res.setHeader("Cache-Control", "max-age=10");
10秒内多次刷新, css和js文件同样没有重新请求,根文件index.html每次都重新请求
同时设置 Cache-control和Expires,Cache-Control优先级更高:
js
// 10秒过期
res.setHeader("Expires", new Date(Date.now() + 1000 * 10).toUTCString());
// 1秒过期
res.setHeader("Cache-Control", "max-age=1");
每隔1秒刷新,每次都请求3个文件
memory cache VS disk cache
都是强制缓存
memory cache 缓存在内存中,读取更快,但是生命周期短,当网页关闭后内存就会释放;
disk cache 缓存在磁盘中,存储时间更长,关闭标签再次进入,被缓存的资源就是disk cache来自硬盘
How does the chrome browser determine memory cache and disk cache ?
可以看一下stack overflow上关于浏览器怎么决定哪些资源是disk chache 还是 memory cache, 网上很多说css是disk cache,不准确,打开淘宝天猫,刷新看Network,css更多的情况是disk cache
图片更多情况是memory cache,刷新掘金首页
协商缓存
用于根文件index.html缓存,响应头返回 Last-Modified 或者 Etag,响应状态码为304 Not Modified,告诉浏览器可以使用缓存,请求头会携带上 if-modified-since 或者 if-none-match
Last-Modified、if-modified-since
上一次请求的响应头返回的Last-Modified ,且该值会在本次请求中,通过请求头 If-Modified-Since 传递给服务端,服务端通过 If-Modified-Since 与资源的修改时间进行对比,若在此日期后资源有更新,则将新的资源发送给客户端。
可以通过node的fs模块中的stat读取文件,拿到文件的ctime,也就是改变时间
json
// fs.stat()返回的文件信息对象
Stats {
dev: 16777237,
mode: 33216,
nlink: 1,
uid: 501,
gid: 20,
rdev: 0,
blksize: 1048576,
ino: 18446744073709552000,
size: 24,
blocks: 256,
atimeMs: 1699969678000,
mtimeMs: 1699956746000,
ctimeMs: 1699956746000,
birthtimeMs: 1699956746000,
atime: 2023-11-14T13:47:58.000Z, //进入时间
mtime: 2023-11-14T10:12:26.000Z, //修改时间
ctime: 2023-11-14T10:12:26.000Z, //改变时间
birthtime: 2023-11-14T10:12:26.000Z
}
js
// main.js
// 强制缓存
res.setHeader("Expires", new Date(Date.now() + 1000 * 10).toUTCString());
res.setHeader("Cache-Control", "max-age=10");
try {
// stat 读取文件,返回有关文件的信息对象
const stats = await stat(filePath);
// 协商缓存
const ctime = stats.ctime.toUTCString();
res.setHeader("Last-Modified", ctime);
// 校验时间
if (req.headers["if-modified-since"] === ctime) {
res.statusCode = 304;
res.end();
return;
}
// ...
}
多次刷新,html文件也不会再次请求
缺点: 通过文件的修改时间来判断资源是否更新不准确,有时候文件更新时间变了,但文件内容未发生更改,也应该重新请求,所以用Etag、if-none-match来判断文件内容是否更新
Etag、if-none-match
服务器端用算法生成该文件的唯一标识,通过设置响应头Etag返回,下一次请求,浏览器会将该值放在请求头if-none-match发给服务器校验
js
// main.js
if (stats.isFile()) {
const fileRes = await readFile(filePath);
//协商缓存
const hash = createHash("md5").update(fileRes).digest("base64");
res.setHeader("Etag", hash);
// 校验资源唯一标识
if (req.headers["if-none-match"] === hash) {
res.statusCode = 304;
res.end();
return;
}
// ...
}
Etag比Last-Modified的优势:
- 文件定期修改,但是文件内容未变,仅仅是改变时间变化,像上面的Stat对象中的ctime改变,客户端没必要重新请求;
- 如果文件在秒内频繁修改,但是If-Modified-Since 能校验的时间只能精确到秒
总结
- 1、强制缓存一般是非html资源文件,协商缓存是根html文件
- 2、强制缓存只要时间没过期就不发请求,协商缓存要发请求校验
- 3、cache-control优先级高于Expires,Etag的优先级高于last-Modified