帮你梳理浏览器缓存机制

237 阅读6分钟

所谓机制,就是说有这么一套规则来束约缓存的逻辑. 束约的工具是浏览器内部实现的. 束约的参数就是我们 HTTP和HTTPS协议的报文内容. 结果就是用户看到的资源.

所以说我们在讨论的浏览器缓存机制的时候,就是在讨论HTTP报文中的某些对应字段的发挥的作用, 辅以一套处理这些字段的逻辑.

这套处理的逻辑可以分为两种强制缓存和协商缓存

强制缓存

在http1.0时代,通过配置expires相应头的属性,设置过期的时间,只要超过这个时间,资源就从服务器上获取.反之就从本地获取.

很简单,但是问题也很多

对本地时间戳过分的依赖,如果客户端本地的时间和服务器的时间不一致的话,那么缓存过期的判断就无法和预期相符

浏览器中的这个时间叫做格林威治时间

所以为了解决这个问题. 在http1.1时代,出现了cache-control字段. 其中的max-age属性就是对于expires的补充.

不是进行替代,而是补充.expires因为简单依旧存在它的使用场景

如下配置, 单位是:

res.writeHead(200m {
    "Cache-control": "max-age:5"
})

这个max-age的含义超过相对时间. 每一次刷新都更新初始化时间,类似防抖函数的作用,当你渲染界面之后的 5 秒钟内,都是可以从缓存中拿到数据的。一旦渲染界面之后,再超出 5 秒钟才再点击拿资源的话,就会重新从服务器上面拿该资源。

很多时候还是需要加上public属性的. "public, max-age:5"的. 含义是: 响应可以被任何对象缓存(包括发送请求的客户端、代理服务器等)

如果只是使用max-age依旧是存在问题的.如果你后台的接口,资源就是在配置的的几秒钟更新了你怎么办?GG了. 所以出现了下面的协商缓存

协商缓存

协商缓存就要求每次都向服务器要结果. 缓存的有效性决定权交给后台.这样自然缓存存在的意义就很大的问题了.对于这个问题暂时不表,先来看看它是如果进行协商缓存的逻辑处理的.

last-modified 实现的协商缓存

这是最简单的协商缓存的方案, 根据文件的修改时间来进行判断. 如下配置

res.setHeader('last-modified', mtime.toUTCString())
res.setHeader('Cache-Control', 'no-cache')

配置成功了之后, 响应头会生成一个属性 if-modified-since. 然后后台再进行如下的判断:

  const ifModifiedSince = req.headers["if-modified-since"];
  if (ifModifiedSince === mtime) {
    // 缓存生效
    res.statusCode = 304;
    res.end();
    return;
  }

如此一来,是能够满足绝大多是的场景的. 但是还是有如下的不足:

  1. 它只是根据时间戳来进行判断,如果只是改变了文件名,而实际内容没有任何改变的情况下,还是会进行服务器的请求拿取.这实在是太蠢了.
  2. 它的单位是秒.如果修改文件的速度非常快,在一些自动化文件处理中.在几百毫秒就完成了.那么它的单位就没有办法通过验证了.

所以为了解决这两点问题, HTTP1.1在随后更新版本中提供etag响应头字段来处理

ETag实现协商缓存

处理逻辑和last-modified基本一致

  s const etag = require("etag");

  const data = fs.readFileSync("./img4.png");
  const etagContent = etag(data);

  const ifNoneMatch = req.headers["if-none-match"];

  if (ifNoneMatch === etagContent) {
    // 缓存生效
    res.statusCode = 304;
    res.end();
    return;
  }

  res.setHeader("etag", etagContent);
  res.setHeader("Cache-Control", "no-cache");
  res.end(data);

etag 表示的是对文件内容的解析进而生成的一个 id,只要文件内容有了改变才会进行变更。自然就能够改变last-modified的两点的不足。它是对其的一个补充方案,而不是替代方案。

etag 依旧带来了新的问题:

  1. 服务器生成文件资源 Etag 需要付出额外的计算开销,如果资源尺寸比较大,数量较多且修改比较频繁的话,那么生成 Etag 的过程显然会印象服务器的性能。
  2. Etag 字段值的生成两种类型,一种是强验证,即更具资源内容的每一个字节来进行验证,最可靠,性能消耗也最大。相对应的就是弱验证,它使用资源内容的部分的属性值来进行生成,生成速度快,但是没有办法很高的成功率。尤其是在服务器集群场景下。

所以说不管哪种缓存方式都有不足,结合具体的场景使用才是正确对待它们的方式. 一般来说,etaglast-modified都是使用的. 所以说对于它们的使用还有一个优先级的问题.

ETag和Last-Modified 的优先级

一般来说,默认配置的话. 是先进行etag的判断的,如果返回的是true的话,再判断last-modified. 当然这个可以后台自己实现自己喜欢的策略.

协商缓存过程的简单总结

可以总结如下图

截屏2022-02-25 16.10.36.png

其他相关配置的豹纹字段

Paragma: no-cache(响应头) HTTP/1.0版本的字段

Cache-Control: 也是操作缓存的.是HTTP/1.1版本字段,向下兼容的,所以说Paragma还是存在的.

Cache-Control的优先级是比前者高的.

Expires: Mon, 15 Aug2016 03:56:47 GMT(格林威治时间)

在HTTP/1.1使用Cache-control中的max-age来代替

Cache-Control的相关属性

  • no-cache: 忽略缓存在本地的副本,强制从服务器上拿资源

  • no-store: 强制缓存在任何情况下都不要保留任何副本

  • max-age=314600: 知识缓存副本的有效时长,从请求时间开始到过期时间之间的描述

  • public: 表明响应可以被任何对象缓存(包括:发送请求的客户端、代理服务器等)

  • private: 表明响应只能被耽搁用户缓存,不能作为共享缓存(即代理服务器不能缓存它)

我们前端需要做些什么

这里是熟悉前端工程话的知识点了.很多 Webpack基础已经帮我们做了. 我们只需要进行进行对应的配置就可以了. 比如说,修改每次打包都修改生成的入口文件的文字

  entry:{
      main: path.join(__dirname,'./main.js'),
      vendor: ['react']
  },
  output:{
      path:path.join(__dirname,'./dist'),
      publicPath: '/dist/',
      filname: 'bundle.[chunkhash].js'
  }
			  

chunkhash就代表出口文件没有打包都会生层对应的hash值. 还有另外两个值可以替换它.hashcontenthash

三者的差别可以用一句话来概括:

  • hash 计算和整个项目的构建相关
  • chunkhash计算同一chunk内容相关
  • contenthash计算和文件内容本身相关 详情可以自己尝试一下.看官网.

还有一些前端世界常听到的:

  • html使用协商缓存
  • css、js、静态资源 使用强缓存,文件名带上hash值