我终于搞懂了http缓存

我终于搞懂了http缓存

前言

做了前端之后才发现,浏览器缓存真是一个神奇的存在,让你又爱又恨,又好奇又敬畏。

在开发过程中,相必各位前端开发经常会有这样的对话:

测试:这个展示有bug

前端:我本地是正常的啊,你清下缓存试试

其实一直以来我对缓存这一块知识都很模糊。前段时间用户有一个问题,后面清了缓存才可以。产品就问我能否上线之后不需要清除缓存,也不是很重要,让我看看能不能搞,不能搞就算了。当时对缓存比较好奇,手下也稍微有点空,就钻研了一段时间。

但是我查到的知识和我们的现有的服务完全匹配不上。理论和实践不对等,这让我对很多东西都更加疑惑。然后,,,然后我就放弃了这个令我望而生畏的领域。产品没再提起这个需求,我也再没看,嘻嘻。

然后呢,最近开发需求效率比较高(闲了起来),想着再啃一啃这块内容,才有了这篇文章。

通过这篇文章你可以了解以下知识:(以下的浏览器只针对谷歌浏览器)

  1. 缓存整个流程(不包括代理服务器缓存,问就是我没有条件和时间测试,后续遇到了再看看~)
  2. http的强缓存和协商缓存
  3. 强缓存和协商缓存出现的必要条件以及如何清除
  4. 磁盘缓存和内存缓存
  5. 浏览器也有自己的强缓存?
  6. 为什么明明服务nginx配置了响应头,但是实际请求的响应头并没有出现
  7. 请求头配置可以解决缓存问题吗?
  8. 使用了微前端-qiankun后发版必须要清缓存才生效吗?原因是什么

缓存流程

drawio

以上示意图仅出现针对未对服务器做特殊配置(如添加expires/Cache-Control等),若修改了服务器配置以下会有另外说明

重点文字说明

对于同一个url资源,不管服务器有没有更新资源,只要浏览器缓存时效未过期,都不会主动向服务器重新请求的

缓存类别

强缓存

强缓存一般是存储在本地的磁盘/内存中,名中强缓存之后的请求的特点:

  1. 没有请求头只有响应头,这个响应头是第一次存储在浏览器的数据

  2. 状态码一般是200,但是带有备注(来自磁盘缓存/来自内存缓存)

命中强缓存必要条件

(任意一条即可)

  1. Cache-Control: max-age=xxx
  2. 响应头有Expires
  3. 响应头存在ETagLast-Modified(协商缓存)且 不存在Cache-Control:no-cache

Cache-Control: max-age和Expires

两者都是规定强缓存存在的时间,命中强缓存规则,除了浏览器自己有一套缓存规则外,服务器可以返回这两个标头来干扰浏览器的缓存规则。

也就是说当响应头存在Cache-Control: max-ageExpires,浏览器会按照这个时间来当做强缓存存在的时间。一旦过了这个时间,浏览器就得重新向服务器发起请求。

如果响应头没有存在Cache-Control: max-ageExpires,但是有Last-Modified/etag,浏览器会按照自己的默认缓存规则来(后面会说这个)

如果响应头没有存在Cache-Control: max-ageExpires,也没有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-ControlExpires

在这个响应头中,Cache-Controlexpires确实都存在,但是Cache-Control有一个值是60,所以最终还是按照60s处理

磁盘缓存和内存缓存

强缓存其实也就是浏览器缓存,但是浏览器缓存存储位置有两个:磁盘缓存和内存缓存,两者有所不同:

  1. 存储介质不同
    1. 磁盘缓存存放在硬盘上,容量更大但速度较慢,加载时间相对较长。
    2. 内存缓存存储在内存中,速度快但容量小,加载时间短。
    3. 缓存对象选择不同
  2. 缓存对象选择不同
    1. 磁盘缓存偏向用于大文件,如图片、视频等。
    2. 内存缓存偏向用于文本类、小文件,如CSS、JS等。
  3. 时效不同
    1. 磁盘缓存可以持续保存,关闭浏览器后不会消失。
    2. 内存缓存只存在于当前会话,关闭页面即被清除。
  4. 控制方式不同
    1. 磁盘缓存需要依赖服务器响应头控制缓存策略。
    2. 内存缓存不依赖响应头,由浏览器自主管理。
  5. 空间限制不同
    1. 磁盘缓存空间较大,但也存在存储限制。
    2. 内存缓存空间受到内存大小限制。

个人觉得对于开发而言,没有必要纠结代码到底是存在磁盘还是内存,仅做了解就可以。所以有关这部分我也没有过多去测试验证。

如何清除跳过强缓存

上面说了,强缓存是有磁盘缓存和内存缓存的

清除内存缓存

  1. 关闭会话

  2. 强制刷新

  3. 清除浏览器缓存

  4. 服务器配置相应响应头

清除磁盘缓存

  1. 物理手段只能通过清除浏览器缓存(对,没错,就是你想的在浏览器设置清除缓存)来跳过

  2. 服务器配置相应响应头

为什么响应头没有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

如何跳过协商缓存

  1. 强制刷新
  2. 清除浏览器缓存
  3. 服务器配置Cache-Control:no-store 或者在服务器端去掉Last-ModifiedETag响应头 去掉Last-ModifiedETag响应头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值

第二次请求资源

  1. 浏览器带上If-Modified-Since(值为上次服务器返回的Last-Modified)和If-None-Match(上次服务器返回的ETag)请求头
  2. 服务器收到请求后,对比当前资源文件的最后修改时间 是否等于 If-Modified-Since 以及资源文件的ETag 是否等于 If-None-Match
  3. 服务器根据对比结果,ETagLast-Modified两个同时对比
    1. 如果两个结果都是相等,表示资源未修改,返回304告知浏览器继续使用缓存
    2. 如果两个结果都是不等,表示资源已修改,返回200状态码和新的Last-Modified、ETag以及资源内容
    3. 如果两个结果出现冲突,已ETag的计算结果为准返回给浏览器
  4. 浏览器处理服务器结果
    1. 收到304则继续使用缓存
    2. 收到200则丢弃旧缓存,保存新Last-Modified、ETag和资源内容

简单粗暴白话文

请求头If-Modified-Since 和服务器自己计算的Last-Modified ,俩值必须一样,不一样就返回新的资源

请求头If-None-Match 和服务器自己计算的ETag ,俩值必须一样,不一样就返回新的资源

请求头If-Modified-Since 的值是上一次服务器计算之后的Last-Modified ,返回给浏览器的,浏览器保存之后用于下一次请求

请求头If-None-Match 的值是上一次服务器计算之后的ETag 返回给浏览器的,浏览器保存之后用于下一次请求

drawio

代理服务器缓存

日后再议~

有关响应头

具体有哪些响应头可以干涉强缓存/协商缓存的?看下面:

强缓存

  • 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-ModifiedETag 响应头,这是由 ngx_http_static_module 模块提供的功能。

原理是 nginx 在响应静态资源请求时,会读取文件的元信息,然后设置:

  • Last-Modified 为文件getmtime(最后修改时间)
  • ETag 为文件inode编号加上修改时间和文件大小的哈希值

这可以让客户端有基于 Last-ModifiedETag 的缓存验证机制。

但是强制缓存如 max-agepublic Cache-Control 指令还需要手动配置。

如果想完全关闭 Last-ModifiedETag 生成,可以主动在nginxlocation块设置:

javascript 复制代码
// 第一种
add_header Last-Modified "";
# 关闭 ETag
etag off;

// 第二种
add_header Cache-Control no-cache;

nginx配置了Cache-Control: max-age,响应头为什么没有生效

这个问题是在我们公司的服务上面确确实实遇到的问题,我们服务器配置了expiresCache-Control no-cache(咱也不知道为什么要把这俩属性配到一起),但是结果就是,实际上服务域名的响应头并没有这两个头头。

去找了运维小哥哥,他肯定地说,这个配置肯定生效了的,不然域名静态资源都获取不了。

我问他有咩有其他配置影响到了,他看了会说确实有。我们服务是有一个网关的,网关的配置覆盖了原有配置,导致之前的不生效。

所以如果nginx配置不生效,勇敢去刚运维。

使用了微前端-qiankun后发版必须要清缓存才生效

这个是我们项目一直都有的问题,但是因为是后管系统,大家的容忍度比较高,没有对这一块的需求提出来。of cource,开发也不主动做(我很忙的了啦,后面再优化了啦)。

但是在学习这一块的时候,我发现了一个盲点:

欸?Vue不同版本打包的chunkhash值是不一样的,那么为什么每次刷新还是加载的旧的chunk

哦,应该是index.html入口文件名称不变,所以导致里面加载的资源还是旧的。

ok,fine,为了验证我刷新了下页面,每次的index.html页面不是304就是200,并没有从缓存获取数据。也就是说每次的index.html都是从服务器获取的,那么为什么还会有旧的index.html

这确实是一个好问题,刚开始我把这个锅归咎于运维(背锅侠)。后面仔细想想应该是浏览器端的缓存导致的问题,与服务器没有关系。所以我又开始陷入瓶颈中...

然后一次偶然的发版,我偶然刷新了子系统,发现子系统仅仅刷新就已经更新了最新版本的代码。但是基座域名下强制刷新也是旧的代码(从浏览器磁盘缓存获取的代码),必须得清浏览器缓存才可以。

也许你可能不了解什么是基座,什么是子系统。我可以这样举例来帮助你了解。

假设,基座:A域名; 子系统1:B域名; 子系统2:C域名; 子系统3:D域名 ,微前端的作用就是不管你访问的是哪个子系统,用户看到的域名永远都是A域名(基座域名)

但是如果你想要访问B域名的b菜单业务功能,你有两个选择:

  1. A域名进入,点击A域名的菜单栏的b菜单,这个时候微前端会加载B域名资源,所以你可以在A域名下看到B域名的功能
  2. B域名进入,找到b菜单,加载业务功能

这两种的根本不同在于,A域名中转加载B域名代码是通过fetch方式,而B域名是直接加载B域名代码,资源类型是document

对浏览器来说,浏览器默认不会对 HTML 文档进行强缓存。每次请求文档时都会查询服务器获取最新版。

但是对于通过代码显式发送的请求,默认为 fetch 请求。fetch 请求浏览器默认是有强缓存的。

结论

通过基座方式加载子系统业务代码,有缓存,原因是请求子系统资源时,发送的为 fetch 请求。fetch 请求浏览器默认是有强缓存的。

直接通过子系统加载业务代码,没有缓存,原因是请求的资源类型是document类型,浏览器默认不会对该类型文档进行强缓存。每次请求文档时都会查询服务器获取最新版。

解决

总不能为了一个缓存,把整个微前端去掉吧。所以终归还是需要在服务器端配置去缓存的指令才可以。

总结

需要想要用户不清除缓存使用新代码,找运维配置服务器对应响应头。

最后给大家推荐一个ai,类似于gpt但比gpt好用的claude.ai/

相关推荐
四喜花露水13 分钟前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
前端Hardy22 分钟前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
web Rookie1 小时前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript
Au_ust1 小时前
css:基础
前端·css
帅帅哥的兜兜1 小时前
css基础:底部固定,导航栏浮动在顶部
前端·css·css3
yi碗汤园1 小时前
【一文了解】C#基础-集合
开发语言·前端·unity·c#
就是个名称1 小时前
购物车-多元素组合动画css
前端·css
编程一生2 小时前
回调数据丢了?
运维·服务器·前端
丶21362 小时前
【鉴权】深入了解 Cookie:Web 开发中的客户端存储小数据
前端·安全·web
Missmiaomiao3 小时前
npm install慢
前端·npm·node.js