Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

理解浏览器缓存 #121

Open
wuxianqiang opened this issue Apr 2, 2019 · 0 comments
Open

理解浏览器缓存 #121

wuxianqiang opened this issue Apr 2, 2019 · 0 comments

Comments

@wuxianqiang
Copy link
Owner

使用缓存的流程图
image
简单来讲,本地没有文件时,浏览器必然会请求服务器端的内容,并将这部分内容放置在本地的某个缓存目录中。在第二次请求时,它将对本地文件进行检查,如果不能确定这份本地文件是否可以直接使用,它将会发起一次条件请求。所谓条件请求,就是在普通的 GET 请求报文中,附带 If- Modified-Since 字段,如下所示:

If-Modified-Since: Sun, 03 Feb 2013 06:01:12 GMT

它将询问服务器端是否有更新的版本,本地文件的最后修改时间。如果服务器端没有新的版本,只需响应一个304状态码,客户端就使用本地版本。如果服务器端有新的版本,就将新的内容发送给客户端,客户端放弃本地版本。代码如下所示:

var handle = function (req, res) {
  fs.stat(filename, function (err, stat) {
    var lastModified = stat.mtime.toUTCString();
    if (lastModified === req.headers['if-modified-since']) {
      res.writeHead(304, "Not Modified");
      res.end();
    } else {
        fs.readFile(filename, function (err, file) {
          var lastModified = stat.mtime.toUTCString();
          res.setHeader("Last-Modified", lastModified);
          res.writeHead(200, "Ok");
          res.end(file);
        });
      }
    });
  }

这里的条件请求采用时间戳的方式实现,但是时间戳有一些缺陷存在。

  1. 文件的时间戳改动但内容并不一定改动。
  2. 时间戳只能精确到秒级别,更新频繁的内容将无法生效。

为此HTTP1.1中引入了ETag来解决这个问题。ETag 的全称是Entity Tag,由服务器端生成,服务器端可以决定它的生成规则。如果根据文件内容生成散列值,那么条件请求将不会受到时间戳改动造成的带宽浪费。下面是根据内容生成散列值的方法:

var getHash = function (str) {
  var shasum = crypto.createHash('sha1');
  return shasum.update(str).digest('base64');
};

If-Modified-Since/Last-Modified 不同的是,ETag的请求和响应是 If-None-Match/ETag ,如下所示:

var handle = function (req, res) {
  fs.readFile(filename, function (err, file) {
    var hash = getHash(file);
    var noneMatch = req.headers['if-none-match'];
    if (hash === noneMatch) {
      res.writeHead(304, "Not Modified");
      res.end();
    } else {
      res.setHeader("ETag", hash);
      res.writeHead(200, "Ok");
      res.end(file);
    }
  });
};

浏览器在收到 ETag: "83-1359871272000" 这样的响应后,在下次的请求中,会将其放置在请求头中: If-None-Match: "83-1359871272000"

尽管条件请求可以在文件内容没有修改的情况下节省带宽,但是它依然会发起一个HTTP请求,使得客户端依然会花一定时间来等待响应。可见最好的方案就是连条件请求都不用发起。那么如何让浏览器知晓是否能直接使用本地版本呢?答案就是服务器端在响应内容时,让浏览器明确地将内容缓存起来。在响应里设置 ExpiresCache-Control 头,浏览器将根据该值进行缓存。那么这两个值有何区别呢?

HTTP1.0时,在服务器端设置 Expires 可以告知浏览器要缓存文件内容,如下代码所示:

var handle = function (req, res) {
  fs.readFile(filename, function (err, file) {
    var expires = new Date();
    expires.setTime(expires.getTime() + 10 * 365 * 24 * 60 * 60 * 1000);
    res.setHeader("Expires", expires.toUTCString());
    res.writeHead(200, "Ok");
    res.end(file);
  });
};

Expires 是一个GMT格式的时间字符串。浏览器在接到这个过期值后,只要本地还存在这个缓存文件,在到期时间之前它都不会再发起请求。

但是 Expires 的缺陷在于浏览器与服务器之间的时间可能不一致,这可能会带来一些问题,比如文件提前过期,或者到期后并没有被删除。在这种情况下, Cache-Control 以更丰富的形式,实现相同的功能,如下所示:

var handle = function (req, res) {
  fs.readFile(filename, function (err, file) {
    res.setHeader("Cache-Control", "max-age=" + 10 * 365 * 24 * 60 * 60 * 1000);
    res.writeHead(200, "Ok");
    res.end(file);
  });
};

上面的代码为 Cache-Control 设置了 max-age 值,它比 Expires 优秀的地方在于,Cache-Control 能够避免浏览器端与服务器端时间不同步带来的不一致性问题,只要进行类似倒计时的方式计算过期时间即可。除此之外, Cache-Control 的值还能设置 publicprivateno-cacheno-store 等能够更精细地控制缓存的选项。

由于在HTTP1.0时还不支持 max-age ,如今的服务器端在模块的支持下多半同时对 ExpiresCache-Control 进行支持。在浏览器中如果两个值同时存在,且被同时支持时, max-age 会覆盖 Expires

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant