如何管理 Web 应用中的 HTTP 缓存策略

201 阅读8分钟

如何管理 Web 应用中的缓存策略

引言

随着 Web 应用变得越来越复杂,资源的数量也日益增加。当用户访问网站时,浏览器会下载大量的 HTML、CSS、JavaScript 以及图片等资源。为了提高性能,浏览器会缓存这些资源,以便下次访问时能更快地加载页面。然而,不恰当的缓存配置可能会导致用户看到过时的内容。因此,了解并正确实施缓存策略对于现代 Web 开发者来说至关重要。

强缓存(Cache-Control)

强缓存是一种客户端(通常是浏览器)在一段时间内直接使用本地缓存内容而无需与服务器进行交互的机制。它依赖于响应头中的Cache-Control字段,其中max-age属性定义了资源的有效期、s-maxage用于共享缓存,如代理服务器、no-store不允许缓存等。

(一)如何设置强缓存

在 Web 服务器端,可以通过设置 HTTP 响应头来控制强缓存的行为。以下是一些常见的设置方式:

(二)Node.js 示例
res.writeHead(200, {
  'Cache-Control': 'max-age=3600', // 缓存一小时
});

(三)强缓存的优势

  • 减少带宽使用: 因为资源从本地缓存加载,所以减少了对服务器的请求,节省了带宽。

  • 加快页面加载速度: 由于不需要等待网络传输时间,页面加载速度显著提高。

  • 减轻服务器负载: 服务器不需要处理重复的请求,从而减轻了负载。

(四)强缓存的挑战

  • 资源更新: 当资源更新时,如果旧的缓存副本仍在有效期内,客户端将继续使用旧的副本,这可能导致用户看到过时的内容。为解决这个问题,通常会采用版本号或哈希值的方式,使得资源 URL 发生变化,从而强制浏览器获取新版本。

  • 兼容性: 虽然Cache-Control是现代 Web 开发的首选,但仍需考虑老旧浏览器的支持情况。

(五)更改强缓存的内容

当服务器上的资源内容发生变更时,如何确保客户端能够及时获取到新的资源版本是一个常见的挑战。以下是一些常用的方法来实现这一目标:

1. 修改文件名

最简单有效的方法是修改文件名。每次发布新版本时,更新资源文件名,使得客户端认为这是一个全新的资源。

示例 :

假设原始文件名为 style.css,更新后改为 style-v2.css。这样,浏览器会认为这是不同的资源,并重新下载。

2. 使用查询字符串

在资源文件的 URL 后面添加一个版本号或时间戳作为查询字符串,这样可以强制浏览器重新下载资源。

示例 :

<link rel="stylesheet" href="/styles/style.css?v=1.2.3">
<script src="/scripts/app.js?timestamp=1693579200"></script>

3. 使用 ETag

ETag是一种标识资源版本的机制,类似于指纹。每当资源发生改变时,服务器返回一个新的 ETag 值。客户端在请求资源时,会带上之前存储的 ETag 值,服务器根据 ETag 判断资源是否已更改。

示例 :

// 服务器端设置 ETag
res.setHeader('ETag', crypto.createHash('md5').update(fs.readFileSync(filePath)).digest('hex'));

// 客户端请求
if (req.headers['if-none-match'] === etag) {
    res.status(304).end();
} else {
    res.setHeader('ETag', etag);
    res.sendFile(filePath);
}

4. 使用Cache-ControlExpires

通过设置Cache-ControlExpires头来控制缓存的有效期。可以在发布新版本时,设置较短的有效期,或者直接设置no-storeno-cache来禁止缓存。

示例 :

res.setHeader('Cache-Control', 'max-age=3600, public'); // 缓存有效期为 1 小时
res.setHeader('Expires', new Date(Date.now() + 3600 * 1000).toUTCString()); // 过期时间为 1 小时后

通过上述方法,可以有效地管理浏览器缓存,确保客户端在资源更新时能够及时获取到新的版本。在实际开发中,可以根据具体需求选择合适的方法来实现强缓存的更新。

协商缓存(Last-Modified/ETag)

协商缓存则是在每次请求时,客户端都会向服务器发起请求,但服务器可以根据条件判断是否需要重新发送资源。这通常通过If-Modified-Since(客户端请求时带上上次请求得到的Last-Modified时间)或If-None-Match(客户端请求时带上上次请求得到的ETag)来进行。如果资源未改变,服务器将返回304状态码,告知客户端使用已有的缓存版本。

(一)协商缓存的工作原理

在 Web 服务器端,可以通过设置 HTTP 响应头来控制协商缓存的行为,协商缓存主要依靠两种机制来实现:

(二)Last-Modified / If-Modified-Since

  • Last-Modified: 当服务器响应客户端请求时,会在 HTTP 响应头中包含一个Last-Modified字段,该字段表示资源最后修改的时间戳。

  • If-Modified-Since: 下次客户端请求同一资源时,会在请求头中包含一个If-Modified-Since字段,该字段包含了上次请求时服务器返回的Last-Modified时间戳。服务器会检查这个时间戳与资源的实际最后修改时间是否一致。如果资源在这段时间内没有被修改,服务器会返回一个 304 状态码,告诉客户端继续使用之前的缓存;如果资源已被修改,服务器则会返回 200 状态码,并附带新的资源内容。

(三)ETag / If-None-Match

  • ETag: ETag是一个由服务器生成的标识符,用来唯一地表示某个特定资源的一个版本。ETag 可以是任何字符串,通常是一个文件内容的哈希值,这样可以更精确地反映文件的变化情况。

  • If-None-Match: 类似于If-Modified-Since,客户端在后续请求中会包含一个If-None-Match字段,该字段包含了上次请求时服务器返回的ETag。服务器会检查这个 ETag 与当前资源的 ETag 是否匹配。如果不匹配,说明资源已经被修改,服务器返回 200 状态码及新的资源内容;如果匹配,则返回 304 状态码。

(四)协商缓存的优势

  • 更精确的资源管理: ETag机制比Last-Modified更精确,因为它可以识别文件内容的微小变化。

  • 减少不必要的传输: 当资源没有变化时,服务器只需要发送少量数据(如304响应),减少了不必要的数据传输。

(四)协商缓存的挑战

  • 性能开销: 与强缓存相比,协商缓存需要更多的服务器处理,因为每次请求都需要检查资源是否被修改。

  • 一致性: 在某些情况下,例如在分布式环境中,保持资源的一致性可能会变得更加复杂。

实践案例分析

在 Node.js 服务器中,我们看看如何实现这两种缓存策略:

const http = require('http');
const path = require('path');
const fs = require('fs');
const mime = require('mime');

const server = http.createServer((req, res) => {
  
  let filePath = path.resolve(__dirname, path.join('www', req.url))

  if (fs.existsSync(filePath)) {
    const stats = fs.statSync(filePath) // 获取该路径对应的资源    
    const isDir = stats.isDirectory() // 判断是否是文件夹
    const { ext } = path.parse(filePath) // 路径的后缀
    if (isDir) {
      filePath = path.join(filePath, 'index.html')
    }

    // 获取前端请求头中的if-modified-since
    const timeStamp = req.headers['if-modified-since']
    let status = 200
    if (timeStamp && Number(timeStamp) === stats.mtimeMs) { // 资源没变更
      status = 304
    }

    if (!isDir && fs.existsSync(filePath)) {
      const content = fs.readFileSync(filePath) // 读取文件
      res.writeHead(status, {
        'Content-type': mime.getType(ext),
        'cache-control': 'max-age=86400',  // 一天
        // 'last-modified': stats.mtimeMs  // 资源最新修改时间
        'etag': '由文件内容生成的hash'  // 文件指纹
      })
      res.end(content)
    } 
  }
})

server.listen(3000, () => {
  console.log('listening on port 3000');
})

代码解析

  1. 文件路径解析:首先根据请求的 URL 确定要服务的文件路径。
  2. 文件存在性检查:使用fs.existsSync()检查文件是否存在。
  3. 文件类型检查:如果是目录,则尝试查找index.html
  4. 协商缓存检查:检查请求头中的if-modified-since,并与文件最后修改时间比较。
  5. 强缓存设置:设置cache-controlmax-age=86400,即缓存一天。
  6. ETag设置:这里虽然设置了一个占位符etag,但实际上应该根据文件内容计算出一个哈希值作为 ETag。

补充知识点

  • ETag 的生成ETag可以是任何字符串,但通常是一个文件内容的哈希值。这样可以更精确地反映文件的变化情况,比仅使用最后修改时间更可靠。
  • 缓存验证请求:当使用协商缓存时,客户端会在请求中包含If-Modified-SinceIf-None-Match头,服务器据此决定是否发送新的资源。
  • 浏览器行为:需要注意的是,某些浏览器在导航预加载过程中可能会忽略Cache-Control头,因此即使设置了no-cache,也可能不会立即生效。

结语

通过合理设置Cache-ControlLast-ModifiedETag等头部信息,我们可以有效地管理 Web 应用中的缓存,从而提升用户体验并减轻服务器负担。掌握这些技巧,将帮助我们在构建高性能 Web 应用时更加游刃有余。希望本文能为你带来启发,并助力你在项目中更好地运用缓存策略。