强缓存(HTTP 1.1 - Cache-Control)
强缓存通过Cache-Control请求头来实现,它允许浏览器存储资源的一个副本,并在特定时间内直接从本地缓存中读取,无需再次请求服务器。
Cache-Control 指令包括:
public:任何缓存都可以存储响应。private:响应是为单个用户准备的,不能被共享。no-cache:缓存必须发送一个请求到服务器以验证缓存数据是否仍然有效。no-store:不允许缓存响应。max-age:指示资源能够被缓存多久(以秒为单位)。s-maxage:替代max-age,但只适用于共享缓存(如代理服务器)。
示例:
Cache-Control: public, max-age=3600
这意味着资源可以被任何缓存存储,并且在接下来的3600秒(1小时)内可以直接从缓存中获取。
协商缓存(ETag/If-None-Match,Last-Modified/If-Modified-Since)
协商缓存依赖于资源的版本标识或最后修改时间,当缓存过期后,浏览器发送请求询问资源是否有更新。
ETag/If-None-Match:
ETag(实体标签)是资源的特定版本的标识符,通常是一个哈希值或版本号。- 当资源过期后,浏览器在请求头
If-None-Match中发送ETag值。 - 服务器比较请求的
ETag和当前资源的ETag,如果相同,则返回304状态码,告诉浏览器使用本地缓存的资源。
Last-Modified/If-Modified-Since:
Last-Modified是资源上次被修改的时间。- 浏览器在后续请求中使用
If-Modified-Since请求头发送上次的修改时间。 - 服务器比较请求的时间和资源的最后修改时间,如果资源未被修改,则返回304状态码。
示例:
ETag: "v=123-4567890"
If-None-Match: "v=123-4567890"
Last-Modified: Wed, 21 Oct 2020 07:28:00 GMT
If-Modified-Since: Wed, 21 Oct 2020 07:28:00 GMT
优点和缺点
强缓存的优点:
- 减少服务器负载,因为不需要对未改变的资源进行重复请求。
- 快速加载资源,因为资源直接从本地缓存获取。
强缓存的缺点:
- 如果资源更新了,但缓存尚未过期,用户可能会看到旧版本的资源。
协商缓存的优点:
- 允许服务器控制缓存的有效性,确保用户获得最新版本的资源。
协商缓存的缺点:
- 需要额外的请求-响应循环来验证资源,可能会增加一些延迟。
使用场景
- 强缓存适用于不经常变动的资源,如静态文件、图片、样式表和脚本。
- 协商缓存适用于可能会更新的资源,需要服务器确认资源的当前状态。
通过合理配置HTTP缓存,可以显著提高Web应用的性能和用户体验。
是
否
客户端请求资源
浏览器检查本地缓存
本地缓存有效
本地缓存失效
服务器返回资源
服务器返回304状态码
使用本地缓存
请求服务器
更新本地缓存
在Node.js中使用Express框架来实现HTTP缓存的代码示例如下:
//创建一个文件夹 进入这个文件夹
npm init
npm install morgan
npm install express
//创建一个文件 将代码放在server.js中
node server.js
强缓存的Node.js实现
const express = require('express');
const app = express();
const morgan = require('morgan');
// 用于开发环境的日志记录中间件
app.use(morgan('dev'));
// 设置静态文件目录
app.use(express.static('public'));
// 强缓存中间件
app.use((req, res, next) => {
// 设置Cache-Control头部,这里设置为缓存一年
res.set({
'Cache-Control': 'public, max-age=31536000'
});
next();
});
// 示例路由
app.get('/', (req, res) => {
res.send('Hello, this is a cached response!');
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
在上面的代码中,我们使用了Express的res.set方法来设置HTTP头Cache-Control,指示浏览器进行强缓存。
协商缓存的Node.js实现
协商缓存需要服务器端和客户端之间进行交互,通常涉及到ETag或Last-Modified头。
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
const morgan = require('morgan');
app.use(morgan('dev'));
// 用于生成ETag的中间件
app.use((req, res, next) => {
const realPath = path.resolve('public', req.path);
const stats = fs.statSync(realPath);
// 设置Last-Modified头部
res.setHeader('Last-Modified', stats.mtime.toUTCString());
// 读取文件内容以生成ETag
const fileContents = fs.readFileSync(realPath);
const ETag = '"' + fileContents.toString('base64') + """;
// 设置ETag头部
res.setHeader('ETag', ETag);
next();
});
// 协商缓存的中间件
app.use((req, res, next) => {
const etag = req.headers['if-none-match'];
const lastModified = req.headers['if-modified-since'];
// 检查ETag
if (etag && etag === res.locals.ETag) {
return res.status(304).end();
}
// 检查Last-Modified
if (lastModified && lastModified === res.locals.LastModified) {
return res.status(304).end();
}
next();
});
// 设置静态文件目录
app.use(express.static('public'));
// 示例路由
app.get('/user', (req, res) => {
res.send('This is a user profile.');
});
app.get('/image', (req, res) => {
res.sendFile(path.join(__dirname, 'public/image.png'));
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
在协商缓存的示例中,我们首先设置Last-Modified和ETag头,然后在中间件中检查请求头中的If-None-Match和If-Modified-Since。如果浏览器提供的值与服务器上的值相匹配,则返回304状态码,告诉浏览器使用本地缓存的资源。
请注意,实际生产环境中生成ETag通常是使用文件内容的哈希值,并且可能涉及到异步操作,因此这里的示例代码为了简化使用了同步方法fs.readFileSync和fs.statSync,这在高流量的生产环境中可能会导致性能问题。在实际应用中,应使用异步方法来避免阻塞Node.js的事件循环。