1、前言
前端缓存主要是分为HTTP缓存
和浏览器缓存
。其中HTTP缓存是在HTTP请求传输时用到的缓存,主要在服务器代码上设置
;而浏览器缓存则主要由前端开发在前端js上进行设置。
http缓存是web缓存的核心,是最难懂的那一部分,也是最重要的那一部分。
2、HTTP缓存位置
从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。
- Service Worker
- 行在浏览器背后的独立线程,一般可以用来实现缓存功能
- 因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS
协议来保障安全 - Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
- Memory Cache
- 内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的
样式、脚本、图片
等。 - 读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。
一旦我们关闭 Tab 页面,内存中的缓存也就被释放了
- 访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存
- Disk Cache
- Disk Cache 也就是存储在硬盘中的缓存,
读取速度慢点
- Disk Cache 比Memory Cache胜在容量和存储时效性上
- 在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的
- 它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求
- Push Cache
- Push Cache是推送缓存,是 HTTP/2 中的内容
- 只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂
- 在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。
如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了。
那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,通常浏览器缓存策略分为两种:强缓存和协商缓存
,并且缓存策略都是通过设置 HTTP Header
来实现的。
3、HTTP缓存-强缓存
对于强制缓存而言,如果浏览器判断所请求的目标资源有效命中,则直接从强制缓存中返回请求响应,无须与服务器进行任何通信。返回200的状态码。
在浏览器控制台NetWork中的体现为:
200 OK (from disk cache)
或者200 OK (from memory cache)
200 OK (from disk cache)
HTTP状态码200,缓存的文件从硬盘中读取200 OK (from memory cache)
HTTP状态码200,缓存的文件从内存中读取
强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。
3.1基于expires实现
request header
: Last-modified
response header
: expires
缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。需要和Last-modified结合使用。Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
- HTTP1.0协议中声明的用来控制缓存失效日期时间戳的字段
- 由服务器端指定后通过响应头告知浏览器,浏览器在接收到带有该字段的响应体后进行缓存。
- 如果浏览器再次发起相同的资源请求,便会对比expires与本地当前的时间戳。
- 当前请求的本地时间戳小于expires的值,则说明浏览器缓存的响应还未过期,无须向服务器端再次发起请求
- 当本地时间戳大于expires值,缓存过期,重新向服务器发起请求。(仅过期才能允许再次发送请求)
- expires判断的局限性
- 对本地时间戳过分依赖
- 如果客户端本地的时间与服务器端的时间不同步,或者对客户端的时间进行主动修改,那么对于缓存过期的判断可能就无法和预期相符
- 解决expires判断的局限性:HTTP1.1协议开始新增了
cache-control
字段
3.2基于cache-control实现
在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存。比如当Cache-Control:max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。
Cache-Control:max-age=N,N就是需要缓存的秒数。从第一次请求资源的时候开始,往后N秒内,资源若再次请求,则直接从磁盘(或内存中读取),不与服务器做任何交互。
Cache-control中因为max-age后面的值是一个滑动时间,从服务器第一次返回该资源时开始倒计时。所以也就不需要比对客户端和服务端的时间,解决了Expires所存在的巨大漏洞。
基于cache-control
实现的强缓存是当下项目中的常规方法,而基于expires
实现的强缓存不被推荐使用。
Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令
- max-age:(单位为s)表示响应资源能被缓存多久
- max-age和expires同时存在,则以max-age为准
- s-maxage:(单位为s)缓存在代理服务器中的过期时长(仅当设置了public属性值时才是有效的)
- max-age和s-maxage并不互斥。他们可以一起使用
- no-cache:强制进行协商缓存
- Cache-control中设置了no-cache,那么该资源会直接跳过强缓存的校验,直接去服务器进行协商缓存
- no-store:禁止使用任何缓存
- 客户端的每次请求都需要服务器端给予全新的响应
- no-cache与no-store是两个互斥的属性值,不能同时设置
- public:表示响应资源既可以被浏览器缓存,又可以被代理服务器缓存
- private:限制了响应资源只能被浏览器缓存
- public和private就是决定资源是否可以在代理服务器进行缓存的属性
- 如果这两个属性值都没有被设置,则默认为private
- public和private 也是一组互斥属性
Cache-control如何设置多个值呢?用逗号分割
Cache-control:max-age=10000,s-maxage=200000,public
如果要考虑向下兼容的话,在Cache-control不支持的时候,还是要使用Expires,这也是我们当前使用的这个属性的唯一理由。
3.3强缓存两种方式的区别
- Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效
- Expires 是http1.0的产物,Cache-Control是http1.1的产物
- expires/cache-control两者同时存在的话,Cache-Control优先级高于Expires
- 在某些不支持HTTP1.1的环境下,Expires就会发挥用处
- Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法
3.4强缓存的缺陷
强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存策略。
4、HTTP缓存-协商缓存
顾名思义,协商缓存就是在使用本地缓存之前,需要向服务器发起一次GET请求,与之协商当前浏览器保存的本地缓存是否已经过期。通常是采用所请求资源的最近一次的修改时间戳来判断的。
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:
-
协商缓存生效(文件未更新),返回304和Not Modified。
-
协商缓存失效(文件更新),返回200和请求结果。
协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag 。
4.1基于last-modified实现
- 首先需要在服务器端读出文件修改时间
- 将读出来的修改时间赋给响应头的last-modified字段
- 最后设置Cache-control:no-cache
第一次请求,服务端代码
const data = fs.readFileSync('./imgs/CSS.png');//读取资源
// 1.读取修改的时间
const { mtime } = fs.statSync('./imgs/CSS.png');
// 2.设置文件最后修改时间
res.setHeader('last-modified',mtime.toUTCString())
// 3.强制设置为协商缓存
res.setHeader('Cache-Control','no-cache');
res.end(data);
第二次及以后的每一次请求,服务器端代码
const data = fs.readFileSync('./imgs/CSS.png');//读取资源
const { mtime } = fs.statSync('./imgs/CSS.png');//读取修改的时间
const ifModifiedSince = req.headers['if-modified-since'];//读取请求头携带的时间(第一次返回给客户端的文件修改时间)
if (ifModifiedSince === mtime.toUTCString()) {
// 如果两个时间一致,则文件没被修改过,返回304
res.statusCode = 304;
res.end();//因为缓存生效,不需要返回数据
return;// 避免返回新的last-modified
}
res.setHeader('last-modified',mtime.toUTCString())// 设置文件最后修改时间
res.setHeader('Cache-Control','no-cache');// 强制设置为协商缓存
res.end(data);
流程
- 客户端第一次请求目标资源的时,服务器返回的响应标头包含last-modified字段,值是该资源的最后一次修改的时间戳,以及cache-control:no-cache
- 当客户端再次请求该资源的时候,会携带一个if-modified-since字段,其值正是上次响应头中last-modified的字段值
- 如果客户端请求头携带的if-modified-since字段对应的时间与目标资源的时间戳进行对比,如果没有变化则返回一个304状态码。
- 如果有变化则返回最新的last-modified标头和Cache-Control:no-cache
last-modified的不足
- last-modified是根据请求资源的最后修改时间戳进行判断的,虽然请求的文件资源进行了编辑,但是内容并没有发生任何变化,时间戳也会更新,从而导致协商缓存时关于有效性的判断验证为失效,需要重新进行完整的资源请求。浪费了网络的带看资源,延长获取资源的时间
- 标识文件资源修改的时间戳单位是秒,如果文件修改的速度非常快,假设在几百毫秒内完成,那么通过时间戳的方式来验证缓存的有效性,是无法识别出该次文件资源的更新的。
综合,以上两种不足可知,基于last-modified实现的协商缓存,服务器无法根据资源修改的时间戳识别出真正的更新,进而导致重新发起了请求
4.2基于Etag实现
为了弥补通过时间戳判断的不足,从HTTP1.1规范开始新增了一个Etag的头信息,即实体标签。 其内容主要是服务器为不同的资源进行哈希计算所生成的一个字符串,该字符串类似于文件指纹,只要文件内容编码存在差异,对应的Etag对文件资源进行更精准的变化感知。
也就是说,ETag就是将原先基于last-modified协商缓存的比较时间戳的形式修改成了比较文件指纹。
文件指纹:根据文件内容计算出的唯一哈希值。文件内容一旦改变则指纹改变。
服务端代码:
流程
- 第一次请求资源时,服务端将要返回给客户端的数据通过ETag模块进行哈希计算生成一个字符串,这个字符串类似于文件指纹。
- 第一次请求资源时,服务端将要返回给客户端的数据通过ETag模块进行哈希计算生成一个字符串,这个字符串类似于文件指纹。
- 检测客户端的请求标头中的if-None-Match字段的值和第一步计算的值是否一致,一致则返回304。
- 如果不一致则返回etag标头和Cache-Control:no-cache
etag的不足
在协商缓存中,Etag并非last-modified的替代方案而是一种补充方案,因为依旧存在一些弊端。
- 服务器对于生成文件资源的Etag需要付出额外的计算开销,如果资源的尺寸比较大,数量较多且修改频繁,那么生成的Etag的过程就会影响服务器的性能。
- Etag字段值的生成分为强验证和弱验证,强验证根据资源内容进行生成,能够保证每个字节都相同,弱验证则根据资源的部分属性来生成,生成速度快但无法确保每个字节都相同,并且在服务器集群场景下,也会因为准确不够而降低协商缓存有效性的成功率,所以恰当的方式是根据具体的资源使用场景选择恰当的缓存校验方式。
4.3Etag/last-modified对比
- 首先在精确度上,Etag要优于Last-Modified。
Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。 - 第二在性能上,Etag要逊于Last-Modified
Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。 - 第三在优先级上,服务器校验优先考虑Etag
5、怎么设置缓存
- 普遍服务器默认开启强缓存
- 缓存是缓存在前端,但实际的代码是后端同学写的,如果需要实现前端缓存的话,通知后端的同学加响应头就好了。