在开发过程中会遇到的缓存问题,主要是由两种方式引起的:
在上述情况中会多次返回 304 状态码,我们知道,在交互中,状态304码表示客户端已经执行 GET,但文件未变化
,所以将使用缓存数据
以上问题都是缓存导致的,那么为什么需要缓存呢,既然会导致数据无法更新等各种问题,为什么还要使用缓存呢?
我们知道一般系统或网页上线之后,如果没有出现更新的情况下,这些静态资源一般是不会发生变化的,那么在每次打开系统的时候重新请求该静态资源就是对网络资源(如是手机网页,就是浪费流量)的浪费,因为之前已经请求过了,所以缓存是很有必要的。
所以就需要一个合适的缓存策略,我们希望数据交互的时候每次都拿到的是最新的数据,不要使用缓存数据;而获取静态资源时,希望当资源发生变更的时获取最新资源,否则使用缓存资源
使用缓存有时会导致意想不到的情况发生,就是资源更新后确使用了缓存的未更新的资源,所以我们要避免这种情况的发生
资源访问的时候直接使用客户端(客户端一般情况是浏览器,也可能是桌面版软件)的缓存资源,避免带宽资源的浪费
若是出现资源的变更,直接使用客户端的缓存资源,会导致不可预测问题
所以希望资源在未发生变更时直接使用缓存资源;若是发生变更希望使用从服务器获取到的变更后资源
客户端获取资源或数据交互的时候,决定是否使用缓存,是客户端(也即浏览器)判定的
。
但是以上说法也并不准确
,是当资源第一次加载完成后,客户端再次需要该资源的时候,根据资源第一次加载的响应指令和页面指令等设置,决定是否使用使用缓存。
如果客户端需要某个资源:
资源允许缓存,并且没有资源更新的情况下
,查看网页的几种情况:
打开新窗口
第一次打开网页,此时浏览器没有缓存信息,资源都是从服务器获取,然后根据缓存策略缓存资源;
再次打开网页,在谷歌浏览器中所有js/css/image资源都是从Disk Cache中获取
地址栏回车
若是第一次访问网页时会访问服务器获取资源;
非第一次访问网页,在该网页打开的情况下回车,也就是刷新网页,因为加载过资源,所以此时内存中是存在该资源的,所以使用资源的情况是从内存中获取;根据浏览器的三级缓存原理,若是页签关闭,内存中资源会被删除,此时新开页签,输入地址回车,因为内存中没有该资源,因此从Disk Cache中加载
按浏览器后退按扭
相当于再次访问网页
按浏览器刷新按扭
等同于地址栏回车
网页浏览器第一次加载的状态码与size信息截图;
上图是一个页面再次加载浏览器信息截图
所以针对状态码与size做一个说明:
状态码/size | 说明 |
---|---|
200/数值大小 | 从服务器下载最新资源,表明之前未加载过该资源,数值是从服务器获取的全部资源大小 |
304/数值大小 | 访问服务器,发现资源没有更新,使用本地资源。数值是与服务器通信报文的大小,并不是资源本身的大小。 |
200/memory cache | 状态码是灰色的,从内存中读取之前已经加载过的资源,不请求服务器,页面关闭时,资源就会被内存释放,再次打开相同页面不会出现此类情况,在同一页面刷新才会出现。一般脚本、字体、图片会存在内存当中 |
200/disk cache | 状态码是灰色的,从磁盘中读取之前已经加载过的资源,不请求服务器,页面关闭不会被释放,这部分资源存在电脑磁盘里,只有用户手动清除浏览器缓存的时候才会释放。一般非脚本会存在磁盘中,如css等 |
状态码 304 ,百度百科上解释说如果客户端发送了一个带条件的 GET 请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变,则服务器应当返回这个 304 状态码。304状态码简单的来说就是服务端的资源未发生变化
客户端是怎么知道这些内容没有更新的呢?其实这不是客户端的事情,而是服务器的事情,服务器可以设置缓存机制,这个功能是为了提高网站的访问速度,当客户端发出 GET 请求的时候,服务器(网页服务器,或者数据交互的服务器)会从缓存中调用你要访问的内容,这个时候服务器就可以判断这个页面是不是更新过了,如果未更新过那么他会给你返回一个 304 状态码。
资源存储在内存中
,当页面关闭时,资源会被内存释放,再次打开该页面资源会重新请求,因为之前被释放掉了;当页面直接刷新时,因为资源没有被释放,所以会从浏览器缓存中获取
内存资源获取速度快,优先级高,但是生命周期短,但是网页关闭后内存会被释放,并且内存大小受限于计算机内存大小,如果存储资源过大仍旧会使用硬盘(磁盘)
资源存储在磁盘中
,如果没有特意清理,资源将始终保存在磁盘上,所以当页面关闭时,资源不会被释放。一般磁盘就是我们所谓的计算机硬盘
硬盘的优点是生命周期长,不触发删除操作,资源不会被删除,缺点是资源获取速度相对内存而言比较缓慢
资源从内存中获取比从磁盘中获取会更快,在内存中获取资源几乎不耗时
可以通过 Cookie 、 Web storage 、IndexDB 存储
Cookie 的存储空间很小,不能超过 4KB,不建议将非用户身份类的数据存储在 Cookie 中,因为 Cookie 随着同域名下每一次资源请求的请求报头传递到服务端进行验证,如果大量非必要的数据存储在 Cookie 中,伴随着请求响应会造成无效资源传输及性能浪费
并且浏览器提供的cookie存储与读取API并不是很好使用
document.cookie='odeon_username=username; domain=cookie.iflytek.com'
// 设置cookie过期时间
let date = new Date()
date.setTime(date.getTime() - 10000)
document.cookie=`odeon_username=username; domain=cookie.iflytek.com; expires=${date.toGMTString()}`
在验证用户身份及维持状态方面,Cookie 有明显的特点和优势,但是数据存储方面并不适合使用cookie
Web storage是H5推出的存储方案,包括 Session Storage 和 Local Storage,存储空间一般为 2.5-10M 之间(不同浏览器存储空间不同)。
Session Storage是临时性存储,是网页会话期间存在,网页关闭后释放。
Local Storage是持久性存储,存储在浏览器本地,除非过期或手动删除,否则一直存在
IndexedDB 是一个大规模的 NoSQL 存储系统,几乎可以存储浏览器中的任何数据内容,包括二进制数据(ArrayBuffer 对象和 Blob 对象),其存储空间一般不少于 250M 的数据。
在使用 IndexedDB 前,需要判断浏览器是否支持:
if (!('indexedDB' in window)) {
console.log('浏览器不支持 IndexedDB')
return
}
在浏览器支持的前提下,便可以对其进行增删改查操作。
let idb
// 打开名为 dbTest ,版本号为 1 的数据库,如果不存在则自动创建
let request = window.indexedDB.open('dbTest', 1)
// 错误回调
request.onerror = function (event) {
console.log('打开数据库失败')
}
// 成功回调
request.onsuccess = function (event) {
idb = request.result
console.log('打开数据库成功')
}
具体的增删改查就不在这里赘述了,感兴趣的可以自己查阅
如果是数据交互的话,我们可以在请求头设置缓存规则;同一个请求根据参数,也即搜索条件有没有发生变化,此时是浏览器策略会先判断是否发生变更,若是参数没有变化,可能直接使用缓存数据,都不去服务去获取新的数据,那么为了避免相同搜索条件使用缓存数据,被浏览器直接拦截,为每次请求添加时间戳参数,或者随机数参数,表示新的请求,去服务器获取数据
一般资源的获取,若是同名的静态资源内部发生了更新的情况,此时根据文件名根本无法判断当前资源已经更新了!!!所以需要其它标志判断,此文件已经更新。目前知道的标志包括Last-Modified
,若是Last-Modified
相同就会返回状态码 304;
但是存在根据浏览器返回的内容显示返回的不是状态码 304,而是 200的场景!!这说明在完全不设置的情况,也就是默认情况应该是返回304,但是当设置了缓存策略后,是否仍从缓存获取资源就要看配置了
发现很多指令,既可以在页面中设置,也可以在nginx 中配置,所以对所有的与缓存可能相关的指令做一个说明:
确认
该资源没有发生变更!!!!max-age=number
,资源过期之后,使其重新生效必须到服务器验证过才可以生效(存在直接使过期缓存生效的场景)CacheControl = no-cache Pragma=no-cache Expires = -1
.HTTP 条件方法可以实现高效的再验证,向服务发送 “条件 GET” 目前最常用的为 If-Modified-Since
:Date, If-None-Match
:ETag(实体标签,版本标识)
If-Modified-Since
表示在指定日期之后资源被更新,就返回新的请求,如果指定日期未更新就返回 304 直接读取缓存
If-None-Match
有的文档有可能周期性的被重写,通过 ETag 来确保文档是否改变,改变,返回请求状态为 200 的新资源;未改变,返回状态码 304 直接读取缓存
这些可以结合使用。
缓存的内容不一定每次都与服务器来进行验证,不同的浏览器对于请求中 Cache-Control 的值和响应中 Cache-Control 的值的优先级是不一样的。
缓存指令挺多的,如果出现冲突的时候,优先级就很重要:
Cache-Control > max-age > Expires
以上指令中可以直接用于html页面的指令包括:
指令在页面中的应用:
<meta http-equiv="Cache-Control" content="no-store" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
根据以上代码,http-equiv 描述要设置的指令,content 表示要设置的指令值;
以下资源都是被index.html中被引入的资源:
刷新后结果
IE:未更新/ Firefox:未更新/ Chrome:更新
解决方案
发生变更后修改 href 添加一个随机数,刷新则随机数不同,即可重新获取图片,不从缓存中获取
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link rel="icon" href="<%= BASE_URL %>favicon.ico?<%= Math.random()%>">
刷新后结果
IE:更新/Firefox:更新/Chrom:更新
解析
js/css 文件更新后浏览器获取到的都是最新的文件,原因是当样式或逻辑变更后打包生成的 js/css 文件的 hash 值也发生变更,因此不存在获取缓存文件的问题,如果是在 html 中直接添加的文件引入,发生变更的情况,也可以添加随机数处理
此种场景需要注意的是刷新后加载的index.html不是缓存文件,否则获取到的 index.html 文件就不是含有变更后hash值名字的 js/css 文件
html 中通过 meta 添加的设置只是限制加载的 html 本页不缓存,html页面本身每次都重新获取,但是 html 页面中发出的请求,不论是静态资源还是数据交互请求,是无法通过meta设置的 no-cache/no-store 控制,并且经过测试,两种值都对页面中的请求没有起作用,IE 一样缓存,该设置只对当前页存在影响,例如:html 发生变化时
Cache-Control 指定请求和响应遵循的缓存机制;设置在请求头或者响应头中
对于 cache-control 的应用有三种,其作用根据重新浏览方式的不同分为以下几种情况:
在请求消息或响应消息中设置 Cache-Control 并不会影响另一个消息处理过程中的缓存处理过程;
请求时的缓存指令值: no-cache、no-store、max-age、max-stale、min-fresh、only-if-cached;
响应时的缓存指令值: no-cache、no-store、max-age、 no-transform、must-revalidate、proxy-revalidate、 public 、 private
各个交互中的指令取值含义如下:
指令 | 说明 |
---|---|
no-cache | 设置请求或响应消息不缓存 |
no-store | 防止重要信息被发布。在请求消息中发送将使请求和响应消息都不使用缓存 |
max-age | 客户端可接收生存期不大于指定时间(以秒为单位)的响应 |
min-fresh | 客户端可接收响应时间小于当前时间+指定时间的响应 |
max-stale | 客户端可接收超出超时期间的响应。如果有值则接收超时后该值的响应消息 |
public | 响应可被任何缓存区缓存 |
private | 对于用户响应消息的缓存不能被共享。此响应消息对其他用户请求无效 |
指令 | 说明 |
---|---|
no-cache | 除非资源进行了再验证,否则客户端不会接受已经缓存的资源 |
no-store | 缓存尽快从存储器中删除资源痕迹 |
max-age=s |
缓存不返回缓存时间>s 秒的文件,定义 max-age 后,缓存内容不一定每次都需要服务器验证,但是过期后一定会向服务器验证,该指令使缓存更加严格 |
max-stale=s |
缓存可随意提供过期文件,如果指定了参数s 在这段时间内,文档不过期 |
min-fresh | 当缓存中有副本文件存在,客户端才会获取副本 |
only-if-cached | 当缓存中有副本文件存在,客户端才会获取副本 |
请求头一般是前端开发中配置的,属于客户端
headers: {
//当只设置cache-control: 'no-cache'时
//IE浏览器始终返回304,抓包工具抓不到包,请求不和服务器确认
//google浏览器始终返回200,抓包工具可以抓取包,请求重新从服务器获取数据,没有利用到浏览器的缓存功能
'cache-control': 'no-cache',
//当只设置Pragma: 'no-cache'时,禁用缓存
//IE浏览器始终返回200,抓包工具可以抓到所有包,请求重新从服务器获取数据,没有利用到浏览器的缓存功能
//google浏览器始终返回200,抓包工具可以抓到所有包,请求重新从服务器获取数据,没有利用到浏览器的缓存功能
'Pragma': 'no-cache'
//两个参数同时不设置时
//IE浏览器始终返回304,抓包工具抓不到包,请求不和服务器确认
//google浏览器首次返回200,之后始终返回304,并且有和服务器确认
//两个参数同时设置时
//IE浏览器始终返回200,抓包工具可以抓到所有包,请求重新从服务器获取数据,没有利用到浏览器的缓存功能
//google浏览器始终返回200,抓包工具可以抓到所有包,请求重新从服务器获取数据,没有利用到浏览器的缓存功能
}
// 请求拦截器:在发送请求前拦截,可设置请求头
axios.interceptors.request.use(
config => {
// config.headers["cache-control"] = 'no-cache';
config.headers["Pragma"] = 'no-cache';
return config
},
error => {
return Promise.reject(error)
}
);
测试发现仅仅设置 Pragma,在参数相同的条件下,浏览器的交互都会取最新的数据,而不是获取浏览器的缓存;
测试若仅仅设置 cache-control,在参数相同的条件下,浏览器的交互是获取浏览器的缓存,无法获取最新数据;
不确定以上是否受到服务端的影响,导致无法获取最新数据,但是可以肯定 Pragma 可以获取最新数据
指令 | 说明 |
---|---|
no-cache | 必须先与代理服务器确认是否更改,然后在在决定使用缓存还是请求 |
no-store | 所有内容都不会被缓存 |
max-age=s |
缓存内容在 s 秒后失效,仅 HTTP1.1 可用,定义了 max-age 之后,缓存的内容不一定每次都需要服务器验证,但是在过期后一定需要服务器验证 |
must-revalidation/proxy-revalidation | 如果缓存内容失效,请求必须发送服务器/代理进行验证 |
public | 所有内容都被缓存 |
private | 仅客户端缓存代理服务器不缓存 |
HttpServletResponse resp = (HttpServletResponse) servletResponse;
resp.setHeader("Access-Control-Allow-Origin", "*");
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Methods","POST, GET, HEAD,PUT,OPTIONS, DELETE,PATCH");
resp.setHeader("Access-Control-Max-Age", "1800");
resp.setHeader("Access-Control-Allow-Headers","Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With,userId,token");
这个是服务端的设置,未测试,暂时未知
如果服务器在响应中设置了 no-cache 即 Cache-Control:no-cache,那么浏览器在使用缓存的资源之前,必须先与服务器确认返回的响应是否被更改,如果资源未被更改,可以避免下载。这个验证之前的响应是否被修改,就是通过上面介绍的请求头 If-None-match 和响应头 ETag 来实现的。
需要注意的是,no-cache 这个名字有一点误导。设置了 no-cache 之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致。如果设置了 no-cache,而 ETag 的实现没有反应出资源的变化,那就会导致浏览器的缓存数据一直得不到更新的情况。
Web缓存性能优化是一种提高网站加载速度和提高用户体验的方法。通过使用缓存,可以减少服务器的负载和网络延迟,从而提高页面的响应速度。以下是一些关于Web缓存性能优化的建议:
强缓存,只要缓存资源不过期就使用缓存资源,一般通过 Expires、Cache-Control 设置缓存时间;
协商缓存,使用缓存资源前,要去服务器校验该资源是否发生变更,变更请求新资源,没有变更使用缓存资源;
启发式缓存,是指没有设置过期时间的情况下(没有响应头 Expires、Cache-Control ),但是浏览器希望尽可能的缓存资源,浏览器设置 space=当前时间减去资源的最后修改时间
,与0比较取较大值(一般肯定是space > 0),此时浏览器将 space 的十分之一设置为资源的过期时间,也就是资源的过期时间=Max[0,space]*0.1
,我们将此种情况称之为启发式缓存
Vue项目,当项目文件发生变化后,打包生成的文件对应的资源文件的hash值都将发生变化,因此不会存在css或者js的缓存问题,唯一有可能引发的问题是index.html文件,因为入口文件始终是index.html,并且打包后名称不会变更!!!!所以若是index.html文件内容发生变更,很可能导致加载的是缓存文件
例如,微服务中项目加载的微服务,在微服务更新后,点击菜单从新打开页面的时候,总是出现页面未更新的情况,此时需要保证index.html文件不缓存,或者是协商缓存(发生变更就要去服务器重新获取)
通过 http-equiv 页面指令设置缓存方式,测试可行
添加时间戳的方式,导致参数不同,每个请求都会到服务器去请求
网上有介绍可以通过nginx配置设置缓存
location ~ .*\.(html)$ { // 对html文件限制缓存
# add_header Cache-Control no-store; 不缓存
add_header Cache-Control no-cache;//替代上面,协商缓存
add_header Pragma no-cache;
}
添加以上代码,希望配置 index.html 的缓存方式,但是该方式连部署都失败了,原因还不明白!!