爱生活,爱编程,学习使我快乐
之前在工作中遇到过一个很让人费解的生产问题,每次前端项目发版后总有个别用户反馈页面白屏,但我和同事的手机访问一直都很正常。每次定位问题都要花很长时间,而且也没能找到问题,然后第二天用户反馈又好了。最后做了一些猜测,可能是浏览器缓存的问题。然后就按这个方向找解决办法,最后是在nginx加了对资源的缓存控制后,再也没有遇到这种问题。所以就收集了相关知识写下了这篇文章,希望可以帮忙更多的朋友。
浏览器缓存是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档。
我们需要避开缺点,让优点最大化。这就需要对我们网站资源的缓存进行合理控制才行。下面会讲到具体如何控制。
在讲几种缓存之前,我们先讲一下http网络协议中和缓存有关的首部信息。
属性名 | 值 | 优先级 | http版本 | 说明 |
---|---|---|---|---|
Cache-control | no-store | 高 | 1.1 | 绝对禁止缓存资源(不缓存) |
— | no-cache | 高 | 1.1 | 浏览器会缓存资源,但每次都会向服务器确认资源是否发生改变(协商缓存) |
— | max-age = xxx | 高 | 1.1 | 缓存时长(缓存的资源将在 xxx 秒后过期) |
— | s-maxage = xxx | 高 | 1.1 | 代理服务器缓存时长(cdn缓存时长,优先级高于max-age与expires) |
— | public | 高 | 1.1 | 客户端和代理服务器均可缓存(允许cdn缓存) |
— | private | 高 | 1.1 | 仅客户端可以缓存(禁止cdn缓存) |
— | must-revalidate | 高 | 1.1 | 如果资源过期,则向服务器获取新资源 |
Expires | Date | 低 | 1.0 | 资源过期时间(依赖客户端时间,容易出现偏差) |
Pragma | no-cache | 低 | 1.0 | 用来向后兼容只支持 HTTP/1.0 协议的缓存服务器,它的行为与 Cache-Control: no-cache 一致 |
Etag | string | 高 | 1.1 | 资源的标识,一般为md5或者hash值 |
Last-modified | Date | 低 | 1.0 | 资源上次修改时间 |
属性名 | 值 | 优先级 | http版本 | 说明 |
---|---|---|---|---|
Cache-control | max-age = xxx | 高 | 1.1 | 缓存时长(缓存的资源将在 xxx 秒后过期) |
— | no-cache | 高 | 1.1 | 浏览器会缓存资源,但每次都会向服务器确认资源是否发生改变 |
Pragma | no-cache | 低 | 1.0 | 用来向后兼容只支持 HTTP/1.0 协议的缓存服务器,它的行为与 Cache-Control: no-cache 一致 |
If-None-Match | string | 高 | 1.1 | 客户端保留的资源标识 |
If-Modified-Since | Date | 低 | 1.0 | 客户端保留的资源上次的修改时间 |
Cache-Contro
和Pragma
是通用首部字段,所以在请求报文和响应报文中都有。本文中我们主要看他在响应报文中的作用。Cache-Contro
的值比较多,最常用的有no-store
、no-cache
、max-age
。其它几个值用的频率并不高,主要是对代理服务器的缓存控制。Cache-Contro
具体使用如下:Cache-Control: private, max-age=0, no-cache
Etag
和请求首页字段If-None-Match
是对双存在的,用来判断服务器资源是否有变更。Last-modified
和请求首页字段If-Modified-Since
是对双存在的,是用来判断服务器资源自上次修改后有没有再被修改。
Pragma
虽是通用首部字段,但只用在客户端发送的请求中。客户端会需求所有的中间服务器不返回缓存的资源。Pragma
是http1.0版本中的首部字段,现在已经被Cache-Contro
所替代。
了解完上面的首页字段后,就可以学习几种缓存方式了。
最常见的缓存有:强制缓存、协商缓存。还有两种不太常见的缓存是:不缓存和启发式缓存。不缓存不算是缓存的一种,但是也算是一般表现形式,所以我也会在下面提到。
强制缓存就是在第一次访问服务器取到数据之后,在过期时间之内不会再去重复请求,而是直接读取本地缓存数据库中的信息(from memory or from disk),两种方式根据浏览器的策略随机获取。
当发起请求的时间超过了设定的时间,即表示资源缓存时间到期,会再次发送请求到服务器重新获取资源。而如果发起请求的时间在限定的时间之内,浏览器会直接读取本地缓存数据库中的信息。
实现这个流程的核心就是如何知道当前时间是否超过了过期时间。强制缓存的过期时间通过第一次访问服务器时返回的响应头获取。在 http 1.0 和 http 1.1 版本中通过不同的响应头字段实现。
在 http 1.0 版本中,强制缓存通过Expires
响应头来实现。 expires 表示未来资源会过期的时间。
在 http 1.1 版本中,强制缓存通过 Cache-Control: max-age=xxx
响应头来实现。
一般来说,为了兼容,两个版本的强制缓存都会被实现。
首页访问时
第二次访问时
从上面可以看出,第一次请求从服务器返回数据。第二次请求直接从本地缓存(disk cache)中返回数据,速度快了很多倍。两次状态码都为200。
强制缓存只有首次请求才会跟服务器通信,读取缓存资源时不会发出任何请求,资源的 Status 状态码为 200,资源的 Size 为 from memory 或者 from disk ,http 1.1 版本的实现(Cache-Control)优先级会高于 http 1.0 版本的实现(Expires)。
存储方式 | memory cache | disk cache |
---|---|---|
存储周期 | 退出进程时数据会被清除 | 退出进程时数据不会被清除 |
存储资源 | 一般脚本、字体、图片会存在内存当中 | 一般非脚本会存在内存当中,如css等 |
协商缓存与强制缓存的不同之处在于,协商缓存每次读取数据时都需要跟服务器通信,并且会增加缓存标识。
在 http 协议的 1.0 和 1.1 版本中也有不同的实现方式。
在第二次请求时,浏览器会将 Last-Modified 的信息放到 If-Modified-Since 请求头去访问服务器。服务器会将 If-Modified-Since 中携带的时间与资源修改的时间匹配,如果时间不一致,服务器会返回新的资源,并且将 Last-Modified 值更新,作为响应头返回给浏览器。如果时间一致,表示资源没有更新,服务器返回 304 状态码,浏览器拿到响应状态码后从本地缓存数据库中读取缓存资源。
这种方式有一个弊端,就是当服务器中的资源增加了一个字符,后来又把这个字符删掉,本身资源文件并没有发生变化,但修改时间发生了变化。当下次请求过来时,服务器也会把这个本来没有变化的资源重新返回给浏览器。
在 http 1.1 版本中,服务器通过 Etag 来设置响应头缓存标识。Etag 的值由服务端生成。
首页请求
再次请求
具体请求响应头信息
协商缓存每次请求都会与服务器交互,第一次是拿数据和标识的过程,第二次开始,就是浏览器询问服务器资源是否有更新的过程。每次请求都会传输数据,如果命中缓存,则资源的 Status 状态码为 304 而不是 200 。同样的,一般来讲为了兼容,两个版本的协商缓存都会被实现,http 1.1 版本的实现优先级会高于 http 1.0 版本的实现。
不缓存就是不管本地缓存是否过期、服务器资源是否有更新,每次都会都从服务器返回资源内容。
使用如下Cache-Control
头信息可实现,值为no-store
而不是no-cache
。
Cache-Control: no-store
如果我们不对资源做任何缓存控制,浏览器又会如何控制缓存呢?接下来我们找几个浏览器测试一下看看结果。
Chrome浏览器
Firefox浏览器
Microsoft Edge浏览器
经测试结果如上,大部分浏览器都和Chrome浏览器是一样的。而Firefox浏览器和Microsoft Edge浏览器又略有不同。所以如果没有设置缓存策略的话,不同浏览器会使用自己的缓存策略,表现各不同。目前看以Chrome浏览器为代表的这种缓存策略是最合理的(接下来的缓存方案中我们会提到)。
可以看出浏览器的默认缓存,有时是协商缓存,有时是强制缓存。那强制缓存有效时间为多少呢?
如果一个可以缓存的请求没有设置Expires和Cache-Control,但是响应头有设置Last-Modified信息,这种情况下浏览器会有一个默认的缓存策略:(当前时间 - Last-Modified)*0.1
,这就是启发式缓存。
目前看来,大部分浏览器都已经实现了,但是彼此也略有不同。
注:只有在服务端没有返回明确的缓存策略时才会激活浏览器的启发式缓存策略。
考虑一个情况,假设你有一个文件没有设置缓存时间,在一个月前你更新了上个版本。这次发版后,你可能得等到3天后用户才看到新的内容了。如果这个资源还在CDN也缓存了,则问题会更严重。
具体需要在nginx中配置各资源的缓存首部字段。具体nginx配置如下:
location /{
root html;
index index.html index.htm;
# 对html文件使用协商缓存
if (request_filename ~* ^.*?.(html|htm)) {
add_header Cache-Control no-cache;
}
# 对css、js、图片、字体、媒体等使用强制缓存(缓存7天)
if (request_filename ~* \.(css|js|png|jpg|jpeg|gif|gz|svg|mp4|ogg|ogv|webm|htc|xml|woff)) {
add_header Cache-Control max-age=604800;
}
# 也可对特定资源不缓存
if (request_filename ~* config\.json) {
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
}
}
除以上的缓存控制外,最好对部署脚本也做一些优化。每次部署前端代码时,不要删除之前的资源,每次都增量部署。这样子的好处是,即使用户端有缓存数据,依然可以从服务器请求到之前的资源,不会因资源加载失败而出现白屏。 缓存控制+增量部署可以做到前端发布对用户毫无影响。
除以上操作之后,用户也可通过浏览器设置来清除浏览器缓存。快捷键是:Shift+Ctrl+Delete。注意不要删除Cookie和浏览记录哦,不然之前保存的一些网站的账号密码和浏览记录就没有了哦。
按F12打开浏览器开发者工具,在控制台Network面板中,勾选Disable cache
后就会禁用缓存。此时强制缓存和协商缓存都不生效。
#
之前。URL拼接时间戳的方法避开缓存很多开发人员基本都会,但是又有很多人并没有正确使用。因为现在单页面应用增加,而很多单页面应用路由使用的是Hash模式,也就是链接中带
#
。如果时间戳加在#
后面,并不会再次发起http请求,所以也就依然会有缓存。
URL中的hash(#号)说明
#
代表网页中的一个位置(锚点)。其右面的字符,就是该位置上的标识符。#
会改变浏览器的访问历史,所以现在#
也被用在一些前端框架的路由中。#
是用来指导浏览器动作的,对服务器端完全无用。所以,HTTP请求中不包括#
。#
不触发网页重新加载。更多关于hash(#号)的内容请点击查看URL中的hash(#号)详解
URL拼接示例
// 原链接
https://www.abc.com/#/index
// 错误的拼接方式
https://www.abc.com/#/index?t=1641805199020
// 正式的拼接方式
https://www.abc.com/?t=1641805199020#/index