最近连续遇到几次浏览器缓存导致的问题,在此总结一下问题的场景、原因以及如何解决,供大家参考。
我们遇到的问题大体上有以下两个场景:
缓存问题场景1:
每当我们发布了新的版本,某个用户打开浏览器,打开了一个崭新的标签页,并访问网站。然而意外地发现页面怎么也打不开了。打开控制台,只看到js或css文件404的报错。。 在等了一段时间之后,他忍不住刷新了一次,才打开页面。
虽然刷新一次就能解决问题,但我比较好奇的有两个点:
- question 1: 为什么每次发布新版本之后,都会有用户打不开页面?
- question 2: 为什么刷新一次之后,页面就可以打开了?
缓存问题场景2:
一点小说明: 堡垒环境是网站的测试环境,一般是快要发布到生产的版本,域名和生产环境一样,公司有一个浏览器插件用来切换堡垒环境和生产环境。
当我们愉快地结束堡垒环境的测试,关闭堡垒测试模式,刷新页面,打算继续使用当前的生产环境时,却发现页面又打不开了,并且即使多次刷新,页面也打不开。
这个场景,我比较好奇的是:
- question 1: 为什么从堡垒环境切换回生产环境时,刷新了也打不开页面?
一点前置小知识
在回答以上场景中的问题之前,我们需要了解一些前置的条件。
- 浏览器是如何加载我们的应用的?
我们这里的应用,都是单页面应用。所以浏览器加载的过程大概是这样:
首先拿到一个html文件,它是整个应用的入口。它引用了所需的其他资源文件 (js文件、css文件)。浏览器再去请求这些文件。等浏览器全部拿到了这些文件,页面就打开了。
- 刷新页面和新标签页打开页面有什么不同?(F5 / Ctrl+R / 浏览器左上角的刷新按钮)
当我们通过这些方式触发刷新的时候,浏览器会给对文件的请求加上一个请求头。Cache-Control: max-age=0。这个字段会触发一次校验,要求浏览器向服务端验证缓存是否有效,确保缓存的文件是最新的版本。
这是一个简版的校验流程,服务端通过对比缓存中文件的最后修改时间和服务端文件的最后修改时间,来判断缓存的文件是否已经失效。这个流程其实就是协商缓存的过程。
这个过程,我个人理解有点像我们吃东西之前要看保质期一样,先确认东西没过期才能吃。当然也可以不看保质期就吃,就有可能吃到过期的东西。
场景1与场景2对比
在了解完前置知识之后,我们从浏览器的角度看一看,这两个场景中到底发生了什么。
- 场景1,发布新版本之后
qustion 1 - answer: 只要用户不是首次访问,并且缓存的文件还在有效期,那么浏览器会直接使用本地缓存的老版本的html文件。之前说它是老版本,是因为我们刚发布过,所以缓存的肯定是版本比较老的文件,线上的是比较新的版本。老版本的html文件引用的也是老版本的js / css文件,但时线上只有新版本的资源。所以这些文件404,找不到,页面就打不开了。
qustion 2- answer: 而刷新会触发刚才我们说的校验过程,浏览器才会发现缓存已经失效了,服务端给它新的文件,浏览器再去请求新版本的js / css文件,页面就能打开了。
- 场景2,从堡垒环境切回生产环境并刷新页面之后
question 1 - answer: 在刷新之前,因为刚访问过堡垒环境,浏览器会缓存堡垒环境的html文件,而且这个html文件的版本一般是比生产环境更新的。而刷新之后,也会校验。但这次校验的结果是缓存是有效的,因为浏览器缓存中的html文件版本更新。浏览器会继续根据这个html文件去请求新版本的js / css文件。但这时生产环境还没有这些文件,这些文件404找不到,页面就会打不开。
- 如何快速解决场景2?
之前我们遇到场景2的问题时,会选择用硬性重加载来解决。快捷键是Ctrl+Shift+R。这种方式可以解决问题,是因为硬性重加载会让请求头多了一个字段:Cache-Control: no-cache。
这个字段的作用是:它要求浏览器不管本地缓存里有没有文件,都会去服务端请求当前最新的资源。如果我们按下了Ctrl+Shift+R,那么虽然我们缓存中的html文件更新,但浏览器会直接向服务端请求。拿到正确版本的入口文件。
如何避免这些缓存问题?
问题的源头: 通过刚才两个场景的对比,可以看出所以的问题都源于那个被缓存的html文件。被缓存的html文件版本与线上的js / css文件版本不对应,才导致了以上的那些缓存问题。
那么,如果我们不缓存这个html文件,总是从服务端取得这个文件,不就可以避免这些缓存问题了么
如何让浏览器不缓存入口html文件?
服务端可以通过一些响应头字段来告诉浏览器是否缓存某个文件。
Cache-Control: no-cache, no-store
Expires: 0
Pragma: no-cache
在响应头中添加以上这些字段以及字段值,相当于告诉浏览器不要缓存返回的文件。至于为什么要加3个字段才可以,具体原因我没有太深究,大概是因为要兼容不同版本的http协议。
而具体实现向响应头中添加这些字段,我们用的是在服务端添加一段代码,对于匹配到根目录,也就是入口html文件的请求,手动地添加这3个字段。
public class HttpInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (request.getServletPath().equals("/")) {
response.addHeader("Cache-Control", "no-cache, no-store");
response.addHeader("Pragma", "no-cache");
response.addHeader("Expires", "0");
};
}
}
添加之后的效果
为了验证,添加这些是否真的起到作用了。我们可以再次刷新页面,如果看到对html文件的请求始终返回200,并且没有from disk cache或者from memory cache等。就说明字段生效了。代表html文件始终是从服务端取得的,而不是从浏览器缓存中得到。
其他试错方案
在很多中文网站上,都看到有往html文件中添加<meta/>标签来阻止浏览器缓存html文件的。但实际这个方式只对IE9及之前的浏览器生效。应该是已经失效的方法。
详情请见Stack Overflow:stackoverflow.com/questions/1…