解决浏览器缓存导致页面非最新的小技巧
为了保证页面访问性能最佳,我们通常在服务端会设置缓存策略,比如说带有 hash 类型的文件会设置过期时间为永久,
非 hash 文件比如 html 等其他文件设置了通用的缓存策略,即:根据 etag 或者 last-modified 来判断文件是否更改,
然后返回 304 代码告知浏览器不用下载,从而保证页面最新。这些策略在页面加载性能和版本维持最新之间保持了平衡。
为什么普通的策略会出现页面非最新?
通常使用上述策略能够保证页面最新。但是有时候会出现两种情况导致页面非最新:浏览器原因和服务端原因。
-
使用 etag 或者 last-modified 存在的问题
etag 是提供文件指纹的标识,这个标识的实现可以通过多种方式,方式的实现关系着文件是否最新。通常情况下 etag 出现无法刷新的情况比较少,
出现问题多是因服务器文件系统或者静态文件服务出现了问题。另一种方式是使用 last-modified(部分etag 也可能使用了时间戳)。last-modified
的精度是精确到秒,对于服务器文件来说这远远不够,1 秒内文件变动可能超过一次,或者出现文件变动但时间戳没有变化的情况,
导致不论客户端怎么刷新都是返回 304 代码,页面无法更新到最新 -
浏览器自身缓存
如果是遵守 http 规范,保证每次页面加载时都使用服务端内容,那么就不存在缓存问题。但实际上浏览器为了提高性能,
总会进行缓存。比如说 safari 下页面会整个被缓存起来,chrome 在输入已有网址时会优化从 dist cache 中获取文件,
而不会去请求服务器。
怎么解决?
对于服务端导致的缓存问题,需要排查出现 304 的原因,针对性的解决,这里就不再介绍了。
对于浏览器的问题,有两种:页面全部缓存和只是缓存文件。
-
全部缓存页面
这种情况通常出现在 safari 后退或者内存不够重新加载时,特点是页面所有信息都被缓存起来,这时候想要重新请求页面文件是没有任何办法的,
只能上显示时判断是否需要进行页面的刷新。由于页面缓存后不执行 onload 事件,需要在 onpageshow 事件中判断是否需要重新加载页面。 -
文件被缓存
前端页面缓存的判断是包含了文件是否为最新的判断。
基础实现
最简单的做法是提供一个 API,保存版本信息,每次加载时请求最新版本信息,
如果不是最新则提示。基本逻辑如下:
fetch('/page/version').then((res) => res.json())
.then((res) => {
if (res && res.version && res.version !== 当前的版本) {
提示或者刷新
}
})
遇到的问题是:当前页面的版本如何保存和怎么变动工作量最小?
版本保存我们可以以文件的方式进行保存,通过 import 后,固定在代码中。这样就可以实现基本的工作。
简化实现
基础实现存在的问题是:版本需要提供独立的 API,对于前端来说配合过程太麻烦。那么我们就考虑下版本信息放在文件中。
放在文件中首先要解决的一个问题是:文件会被缓存,而且比较严重。解决这个问题就在 url 中添加时间戳,保证每次 URL 不同,
最终做法如下:
- version 文件存放在 static 或者 public 文件下,这样在打包后会将该文件复制到文件下。
- 当前版本保存:通过js import 进文件,导入变量
- 获取服务端版本:请求静态文件(添加时间戳保证不被缓存
- 对比:服务端版本存在且不等于当前版本,提供通知,让用户手动刷新,或者直接使用 window.location.reload(true)
代码如下(使用 antd 中的 notication 进行通知,其他类型的通知也类似):
import { notification } from 'antd'
import page from '../../public/pageVersion.json'
fetch(`/pageVersion.json?_=${Date.now()}`).then(res => res.json())
.then((res) => {
if (res.version && page.version !== res.version) {
notification.open({
message: '页面过期',
description: `当前页面已经过期,最近更新时间为${res.updateTime},请手动刷新浏览器页面以便获取更好体验`,
duration: null
})
}
})
export default {}
当然,你可以做一些交互或者其他更复杂的操作,完全看个人需求了。