Http缓存

42 阅读4分钟

强缓存(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,指示浏览器进行强缓存。

image-20240523205849423

image-20240523210021321

协商缓存的Node.js实现

协商缓存需要服务器端和客户端之间进行交互,通常涉及到ETagLast-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-ModifiedETag头,然后在中间件中检查请求头中的If-None-MatchIf-Modified-Since。如果浏览器提供的值与服务器上的值相匹配,则返回304状态码,告诉浏览器使用本地缓存的资源。

请注意,实际生产环境中生成ETag通常是使用文件内容的哈希值,并且可能涉及到异步操作,因此这里的示例代码为了简化使用了同步方法fs.readFileSyncfs.statSync,这在高流量的生产环境中可能会导致性能问题。在实际应用中,应使用异步方法来避免阻塞Node.js的事件循环。