HTTP 缓存原理

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术,通过复用以前获取的资源,可以显着提高网站性能,降低服务器处理压力。在不考虑代理服务器和私有缓存等情况下,可以简单理解缓存有两种

客户端强缓存

强缓存是指浏览器访问服务器获取到资源后会把资源内容缓存在本地,在一定的时间内发起相同的请求直接使用缓存内容,不再向服务器发起请求,缓存有效时间通过服务器写入 response 的 header 指定

  • HTTP 1.0 规定使用 Expires 指定一个绝对时间,但不同地区使用的时区不同,时间也就不同,经常会出差错
  • HTTP 1.1 使用 Cache-Control 指定一个相对时间,也就是距本次响应 xxx 秒后过期,解决了 Expires 的问题

    客户端服务器协商缓存

    当客户端强缓存过期后浏览器重新向服务器获取最新资源,但有可能服务器资源从来就没变过,这样浏览器其实可以继续使用本地缓存,没必要让从服务器再获取一遍一样的资源

只要每次服务器返回客户端内容的时候发送一个标志,每次浏览器请求的时候服务器可对应标志做一下对比,没有变化就告知浏览器使用本地资源就可以了(直返回状态码 304,不返回具体内容),同样两代 HTTP 协议给了不同的实现方式

对比时间

HTTP 1.0 会在浏览器第一次请求资源的时候把文件最新修改时间写入响应头 Last-Modified ,浏览器下次发请求来的时候会在请求头 If-Modified-Since 携带这个时间,服务器可以对比浏览器本地缓存资源的修改时间和服务器当下的修改时间是否一致,来决定是否返回最新内容

这样的方式有几个弊端

  1. 如果一个文件被误修改,然后修改被撤销,这样内容虽然没变化,但最新修改时间会变
  2. 文件被周期性的修改,文件内容没有变化,但最新修改时间会变化

    对比内容

    HTTP 1.1 中用文件内容来判断缓存是否可用,当然内容一般都 hash 处理成一个固定长度字符串,第一次请求响应头写入 ETag ,浏览器第二次请求携带请求头 If-None-Match

这样其实也有几个弊端

  1. ETag 生成有一定的开销,如果文件频繁变化对服务器有额外压力
  2. 有时候客户端对文件内容变化不敏感的时候,精准的 ETag 判断会让缓存失效概率大增

不同于强缓存 Expires 完全被废弃,协商缓存的两种方式要根据实际应用场景酌情使用,同时使用 Etag 优先级高

完整请求流程

image.png

代码实现

首先在 config.js 添加缓存相关配置

  1. // 静态资源服务器默认配置
  2. module.exports = {
  3. port: 9527,
  4. root: process.cwd(), // 默认使用启动 node 的目录做为根目录
  5. maxAge: 60 * 1000, // 本地缓存时间,默认 60s
  6. enableEtag: true,
  7. enableLastModified: true,
  8. };

添加专门处理 cache 的模块 cache.js

  1. const fs = require('fs');
  2. const path = require('path');
  3. const etag = require('etag');
  4. const { root, maxAge, enableEtag, enableLastModified } = require('./config');
  5. function handleCache(req, res) {
  6. if(maxAge) {
  7. res.setHeader('Cache-Control', `max-age=${maxAge}`);
  8. }
  9. if (!enableEtag & !enableLastModified) {
  10. res.statusCode = 200;
  11. }
  12. const { url, headers } = req;
  13. const filePath = path.join(root, url);
  14. if (enableEtag) {
  15. const reqEtag = headers['if-none-match'];
  16. // 为了方便演示,使用同步方法读取响应内容,计算 etag,正常逻辑不会这么处理
  17. const resEtag = etag(fs.readFileSync(filePath));
  18. res.setHeader('ETag', resEtag);
  19. res.statusCode = reqEtag === resEtag ? 304 : 200;
  20. }
  21. if (enableLastModified) {
  22. const lastModified = headers['if-modified-since'];
  23. const mtime = fs.statSync(filePath).mtime.toUTCString();
  24. res.setHeader('Last-Modified', mtime);
  25. res.statusCode = lastModified === mtime ? 304 : 200;
  26. }
  27. }
  28. module.exports = handleCache;

在主模块引用

  1. const handleCache = require('./cache');
  2. ...
  3. handleCache(req, res);
  4. // 304 不再返回 body 内容
  5. if (res.statusCode !== 304) {
  6. if (compression) {
  7. // 指定服务器使用的压缩方式,浏览器使用对应的解压方式
  8. res.setHeader('Content-Encoding', compression.method);
  9. fs.createReadStream(filePath).pipe(compression.stream).pipe(res);
  10. } else {
  11. fs.createReadStream(filePath).pipe(res);
  12. }
  13. } else {
  14. res.end('');
  15. }

使用浏览器测试,第一次访问
image.png
默认本地缓存一分钟,一分钟之内访问会使用浏览器缓存
image.png
一分钟之后访问会从服务器得到 304 状态码,修改服务器内容后访问会得到 200 & 新的响应内容,替换本地缓存
image.png

使用 Chrome 开发者工具查看效果有两个要注意地方

  1. 在网络面板取消【禁用缓存】选项
  2. 不要使用浏览器刷新(request 会把 max-age 设置为 0,让浏览器本地缓存自动失效)

image.png

完整代码:https://github.com/Samaritan89/static-server/tree/v4
修改部分:https://github.com/Samaritan89/static-server/pull/2/commits/04a754b6e914df7528ed15743f5e97f10c451422