我终于搞懂了http缓存
前言
做了前端之后才发现,浏览器缓存真是一个神奇的存在,让你又爱又恨,又好奇又敬畏。
在开发过程中,相必各位前端开发经常会有这样的对话:
测试:这个展示有bug
前端:我本地是正常的啊,你清下缓存试试
其实一直以来我对缓存这一块知识都很模糊。前段时间用户有一个问题,后面清了缓存才可以。产品就问我能否上线之后不需要清除缓存,也不是很重要,让我看看能不能搞,不能搞就算了。当时对缓存比较好奇,手下也稍微有点空,就钻研了一段时间。
但是我查到的知识和我们的现有的服务完全匹配不上。理论和实践不对等,这让我对很多东西都更加疑惑。然后,,,然后我就放弃了这个令我望而生畏的领域。产品没再提起这个需求,我也再没看,嘻嘻。
然后呢,最近开发需求效率比较高(闲了起来),想着再啃一啃这块内容,才有了这篇文章。
通过这篇文章你可以了解以下知识:(以下的浏览器只针对谷歌浏览器)
- 缓存整个流程(不包括代理服务器缓存,问就是我没有条件和时间测试,后续遇到了再看看~)
- http的强缓存和协商缓存
- 强缓存和协商缓存出现的必要条件以及如何清除
- 磁盘缓存和内存缓存
- 浏览器也有自己的强缓存?
- 为什么明明服务nginx配置了响应头,但是实际请求的响应头并没有出现
- 请求头配置可以解决缓存问题吗?
- 使用了
微前端-qiankun
后发版必须要清缓存才生效吗?原因是什么
缓存流程
以上示意图仅出现针对未对服务器做特殊配置(如添加expires
/Cache-Control
等),若修改了服务器配置以下会有另外说明
重点文字说明
对于同一个url资源,不管服务器有没有更新资源,只要浏览器缓存时效未过期,都不会主动向服务器重新请求的
缓存类别
强缓存
强缓存一般是存储在本地的磁盘/内存中,名中强缓存之后的请求的特点:
-
没有请求头只有响应头,这个响应头是第一次存储在浏览器的数据
-
状态码一般是200,但是带有备注(来自磁盘缓存/来自内存缓存)
命中强缓存必要条件
(任意一条即可)
Cache-Control: max-age=xxx
- 响应头有
Expires
- 响应头存在
ETag
和Last-Modified
(协商缓存)且 不存在Cache-Control:no-cache
Cache-Control: max-age和Expires
两者都是规定强缓存存在的时间,命中强缓存规则,除了浏览器自己有一套缓存规则外,服务器可以返回这两个标头来干扰浏览器的缓存规则。
也就是说当响应头存在Cache-Control: max-age
或Expires
,浏览器会按照这个时间来当做强缓存存在的时间。一旦过了这个时间,浏览器就得重新向服务器发起请求。
如果响应头没有存在Cache-Control: max-age
或Expires
,但是有Last-Modified/etag
,浏览器会按照自己的默认缓存规则来(后面会说这个)
如果响应头没有存在Cache-Control: max-age
或Expires
,也没有Last-Modified/etag
,浏览器每次都必须发送新的请求,因为缓存失效之后是否可以使用缓存,还是需要Last-Modified/etag
。
max-age
- 指定缓存的最大寿命时间,单位是秒。
- 相对于请求时间,例如
max-age=3600
表示可以缓存3600秒。
Expires
- 指定一个绝对的缓存过期时间,
response
发送时间为基准。 - 例如
Expires: Thu, 01 Dec 2022 16:00:00 GMT
如果两者冲突(在http规范中)
Cache-Control
的优先级更高。max-age
会覆盖Expires的设置。- 以
max-age
为准。
如果你在nginx中这样配置呢?
javascript
expires 60s;
add_header Cache-Control max-age=10;
最终会按照哪一个时间处理呢?答案是60s
你就要问了,你这矛盾啊,不是说Cache-Control
的优先级更高嘛?
实际上nginx
配置的那个expires
指令,会同时转换成http的Cache-Control
和Expires
头
在这个响应头中,Cache-Control
和expires
确实都存在,但是Cache-Control
有一个值是60,所以最终还是按照60s处理
磁盘缓存和内存缓存
强缓存其实也就是浏览器缓存,但是浏览器缓存存储位置有两个:磁盘缓存和内存缓存,两者有所不同:
- 存储介质不同
- 磁盘缓存存放在硬盘上,容量更大但速度较慢,加载时间相对较长。
- 内存缓存存储在内存中,速度快但容量小,加载时间短。
- 缓存对象选择不同
- 缓存对象选择不同
- 磁盘缓存偏向用于大文件,如图片、视频等。
- 内存缓存偏向用于文本类、小文件,如CSS、JS等。
- 时效不同
- 磁盘缓存可以持续保存,关闭浏览器后不会消失。
- 内存缓存只存在于当前会话,关闭页面即被清除。
- 控制方式不同
- 磁盘缓存需要依赖服务器响应头控制缓存策略。
- 内存缓存不依赖响应头,由浏览器自主管理。
- 空间限制不同
- 磁盘缓存空间较大,但也存在存储限制。
- 内存缓存空间受到内存大小限制。
个人觉得对于开发而言,没有必要纠结代码到底是存在磁盘还是内存,仅做了解就可以。所以有关这部分我也没有过多去测试验证。
如何清除跳过强缓存
上面说了,强缓存是有磁盘缓存和内存缓存的
清除内存缓存
-
关闭会话
-
强制刷新
-
清除浏览器缓存
-
服务器配置相应响应头
清除磁盘缓存
-
物理手段只能通过清除浏览器缓存(对,没错,就是你想的在浏览器设置清除缓存)来跳过
-
服务器配置相应响应头
为什么响应头没有max-age/Expires,浏览器也命中了强缓存
浏览器其实本身就有自己的缓存规则,当没有服务器的干预情况下,一般都会按照浏览器的缓存规则来做一定的缓存处理,,且这个强缓存存在的时间是:(【文件访问时间】-【文件修改时间】)/10
该测试中,文件上次修改的时间为10:21:40 , 文件第一次访问的时间10:47.01,文件第二次访问的时间(强缓存已失效)10:49.46
所以【预计强缓存存在时间】= (10:47.01-10:21:40)/10 约= 155.2s
【实际强缓存存在时间】 = (10:49.46-10:47.01)/10 约= 165s
且因为强缓存刚好失效的时间点我没有办法刚好访问到,所以【实际强缓存存在时间】是会比【预计强缓存存在时间】大一些
,这个是在正常的误差范围。
协商缓存
协商缓存是指【浏览器】和【服务器】之间的协商,一般从服务器下载的资源会比较大。但是如果是协商缓存,服务器并不会直接把资源给你,而是告诉你可以从缓存拿取数据,这样响应数据比较小,加载时间更短。
协商缓存的工作流程
- 客户端发送带验证字段的conditional请求
- 服务器端接收请求,比对资源验证信息
- 如果资源未修改,返回304状态告知继续使用缓存
- 如果资源已修改,返回200状态和新的资源
协商缓存存在的标志是:服务器端返回包含资源标识的验证字段,如Last-Modified或ETag
如何跳过协商缓存
- 强制刷新
- 清除浏览器缓存
- 服务器配置
Cache-Control:no-store
或者在服务器端去掉Last-Modified
或ETag
响应头 去掉Last-Modified
或ETag
响应头nginx配置:
javascript
add_header Last-Modified "";
# 关闭 ETag
etag off;
ETag和Last-Modified
Last-Modified
- 表示资源内容上次被修改的时间
- 浏览器使用
If-Modified-Since
请求头进行验证 - 时间精确到秒级,可能存在冲突
ETag
- 是内容摘要,能唯一标识版本
- 浏览器使用
If-None-Match
请求头验证 - 以Hash形式展现,不会暴露真实文件路径
协商缓存的整个过程描述
第一次请求服务器
- 浏览器请求不包含任何验证字段
- 服务器返回200状态码、Last-Modified时间戳、ETag签名和完整资源
- 浏览器保存资源内容,以及Last-Modified和ETag值
第二次请求资源
- 浏览器带上
If-Modified-Since
(值为上次服务器返回的Last-Modified
)和If-None-Match
(上次服务器返回的ETag
)请求头 - 服务器收到请求后,对比当前
资源文件的最后修改时间 是否等于 If-Modified-Since
以及资源文件的ETag 是否等于 If-None-Match
- 服务器根据对比结果,
ETag
和Last-Modified
两个同时对比- 如果两个结果都是相等,表示资源未修改,返回304告知浏览器继续使用缓存
- 如果两个结果都是不等,表示资源已修改,返回200状态码和新的Last-Modified、ETag以及资源内容
- 如果两个结果出现冲突,已ETag的计算结果为准返回给浏览器
- 浏览器处理服务器结果
- 收到304则继续使用缓存
- 收到200则丢弃旧缓存,保存新Last-Modified、ETag和资源内容
简单粗暴白话文
请求头If-Modified-Since
和服务器自己计算的Last-Modified
,俩值必须一样,不一样就返回新的资源
请求头If-None-Match
和服务器自己计算的ETag
,俩值必须一样,不一样就返回新的资源
请求头If-Modified-Since
的值是上一次服务器计算之后的Last-Modified
,返回给浏览器的,浏览器保存之后用于下一次请求
请求头If-None-Match
的值是上一次服务器计算之后的ETag
返回给浏览器的,浏览器保存之后用于下一次请求
代理服务器缓存
日后再议~
有关响应头
具体有哪些响应头可以干涉强缓存/协商缓存的?看下面:
强缓存
Cache-Control: max-age
- 设置最大缓存时效(强缓存的时效)Cache-Control: no-cache
- 需要对缓存进行验证(禁用强缓存)Cache-Control: no-store
- 禁止缓存(强缓存和协商缓存都禁用)Expires
- 设置绝对过期时间(强缓存的时效)Pragma: no-cache
- 兼容HTTP 1.0的不缓存指令
协商缓存
Last-Modified
- 资源上次修改时间,用于If-Modified-Since
验证ETag
- 资源唯一标识,用于If-None-Match
验证
缓存范围
Cache-Control: public
- 允许客户端和CDN缓存Cache-Control: private
- 只允许客户端浏览器缓存Cache-Control: s-maxage
- 只允许CDN等代理服务器缓存
其他
- Vary - 区分不同版本的缓存
- Age - 代理服务器上的缓存时间
- Expires - 控制缓存失效时间
客户端请求头
在解决强缓存期间,我看了很多文章都说需要在请求头加上Cache-Control: no-cache
就可以,但是实际实践的结果并不理想。浏览器还是老样子,并没有按照请求头指令执行。
后面看了其他资料,了解到其实请求头的指令,浏览器会当做建议处理,实际上影响浏览器缓存的因素过多(像文件修改时间,文件大小等),请求头只是一个建议而已,浏览器不一定会采取的。可以不负责任的说:
服务器就是爷爷,浏览器是爸爸,客户端请求头就是儿子。
儿子的建议,爸爸不一定会采纳;但是爷爷的指令,爸爸一定会遵循。
所以如果想要清除缓存,建议还是在服务器端设置。
遇到的问题
为什么nginx没有配置协商缓存有关指令,响应头还是自带了ETag和Last-Modified?
对于静态资源文件如 JS、CSS、图片等,nginx
默认情况下会自动生成Last-Modified
和 ETag
响应头,这是由 ngx_http_static_module
模块提供的功能。
原理是 nginx
在响应静态资源请求时,会读取文件的元信息,然后设置:
Last-Modified
为文件getmtime
(最后修改时间)ETag
为文件inode
编号加上修改时间和文件大小的哈希值
这可以让客户端有基于 Last-Modified
和 ETag
的缓存验证机制。
但是强制缓存如 max-age
、public
等 Cache-Control
指令还需要手动配置。
如果想完全关闭 Last-Modified
和 ETag
生成,可以主动在nginx
的location
块设置:
javascript
// 第一种
add_header Last-Modified "";
# 关闭 ETag
etag off;
// 第二种
add_header Cache-Control no-cache;
nginx配置了Cache-Control: max-age,响应头为什么没有生效
这个问题是在我们公司的服务上面确确实实遇到的问题,我们服务器配置了expires
和 Cache-Control no-cache
(咱也不知道为什么要把这俩属性配到一起),但是结果就是,实际上服务域名的响应头并没有这两个头头。
去找了运维小哥哥,他肯定地说,这个配置肯定生效了的,不然域名静态资源都获取不了。
我问他有咩有其他配置影响到了,他看了会说确实有。我们服务是有一个网关的,网关的配置覆盖了原有配置,导致之前的不生效。
所以如果nginx配置不生效,勇敢去刚运维。
使用了微前端-qiankun后发版必须要清缓存才生效
这个是我们项目一直都有的问题,但是因为是后管系统,大家的容忍度比较高,没有对这一块的需求提出来。of cource,开发也不主动做(我很忙的了啦,后面再优化了啦)。
但是在学习这一块的时候,我发现了一个盲点:
欸?Vue不同版本打包的chunk
的hash
值是不一样的,那么为什么每次刷新还是加载的旧的chunk
?
哦,应该是index.html
入口文件名称不变,所以导致里面加载的资源还是旧的。
ok,fine,为了验证我刷新了下页面,每次的index.html
页面不是304就是200,并没有从缓存获取数据。也就是说每次的index.html
都是从服务器获取的,那么为什么还会有旧的index.html
?
这确实是一个好问题,刚开始我把这个锅归咎于运维(背锅侠)。后面仔细想想应该是浏览器端的缓存导致的问题,与服务器没有关系。所以我又开始陷入瓶颈中...
然后一次偶然的发版,我偶然刷新了子系统,发现子系统仅仅刷新就已经更新了最新版本的代码。但是基座域名下强制刷新也是旧的代码(从浏览器磁盘缓存获取的代码),必须得清浏览器缓存才可以。
也许你可能不了解什么是基座,什么是子系统。我可以这样举例来帮助你了解。
假设,基座:A域名;
子系统1:B域名;
子系统2:C域名;
子系统3:D域名
,微前端的作用就是不管你访问的是哪个子系统,用户看到的域名永远都是A域名(基座域名)
但是如果你想要访问B域名的b菜单业务功能,你有两个选择:
- 从
A域名
进入,点击A域名
的菜单栏的b菜单
,这个时候微前端会加载B域名
资源,所以你可以在A域名
下看到B域名
的功能 - 从
B域名
进入,找到b菜单
,加载业务功能
这两种的根本不同在于,A域名中转加载B域名
代码是通过fetch
方式,而B域名
是直接加载B域名代码,资源类型是document
。
对浏览器来说,浏览器默认不会对 HTML 文档进行强缓存。每次请求文档时都会查询服务器获取最新版。
但是对于通过代码显式发送的请求,默认为 fetch
请求。fetch
请求浏览器默认是有强缓存的。
结论:
通过基座方式加载子系统业务代码,有缓存,原因是请求子系统资源时,发送的为 fetch
请求。fetch 请求浏览器默认是有强缓存的。
直接通过子系统加载业务代码,没有缓存,原因是请求的资源类型是document
类型,浏览器默认不会对该类型文档进行强缓存。每次请求文档时都会查询服务器获取最新版。
解决:
总不能为了一个缓存,把整个微前端去掉吧。所以终归还是需要在服务器端配置去缓存的指令才可以。
总结
需要想要用户不清除缓存使用新代码,找运维配置服务器对应响应头。
最后给大家推荐一个ai,类似于gpt但比gpt好用的claude.ai/