HTML5 缓存问题方案及自动检测更新方案
前言
浏览器的缓存,一般情况下,是会提升用户体验的。但在项目更新上线时,又可能会对我们产生困扰,比如因为浏览器已缓存,用户看到的还是旧的页面、旧的图片、旧的样式等等,导致成为了"错误",这种情况是我们在做项目更新时不希望看到的。
正文
我们如何保证项目、代码升级时,用户那里会渲染最新的程序呢?因为在项目升级时,我们不能让每个用户都手动刷新。
1. 从了解浏览器缓存开始
浏览器缓存是指当客户端向服务器请求资源时,会先抵达浏览器缓存,如果浏览器有"要请求资源"的副本,就可以直接从浏览器缓存中提取而不是从原始服务器中提取这个资源。浏览器缓存分为:
协议缓存 :也叫浏览器缓存、网页缓存,通过协议头里的 Cache-Control、Last-Modified、Expires、Etag等控制文件缓存;
应用缓存 :缓存HTML5程序,让Web应用程序可以离线运行;
移动端APP中的内嵌HTML5缓存:也可以理解为 webview 中的 缓存;
协议缓存和 webview中缓存的场景出现的比较多,本文中只聊这两种缓存。
2. http协议缓存常用参数
http协议缓存机制是指通过 HTTP 协议头里的 Cache-Control(或 Expires)和 Last-Modified(或 Etag)等字段来控制资源缓存的机制。
http协议缓存常用参数:
参数名 | 作用 | 常用参数值 |
---|---|---|
Cache-Control | HTTP1.1标准。用于控制资源在本地缓存有效时长,以从现在起的相对秒数计算 | 如 Cache-Control:max-age=600 表示文件在本地缓存有效时长是600秒。在600秒内,如果有请求这个资源,直接使用本地缓存的文件;no-cache,绕开浏览器缓存,每次请求都询问服务端;no-store,不缓存,客户端、服务器都不缓存;must-revalidate,在资源过期后,必须重新询问服务器。 |
Expires | HTTP1.0标准。设置资源在本地缓存到期时间戳,如果客户端和服务器的时间不同,可能会导致缓存的失效不能精确的按服务器的预期运行 | Expires: Sun, 10 Dec 2023 23:59:59 GMT;如果和Cache-Control同时存在,那么Cache-Control的优先级更高 |
Last-Modified | 记录资源最后修改的时间。缓存验证,响应头中返回。Cache-Control设置为no-cache时会启用。 | 如:Last-Modified: Sun, 10 Dec 2023 23:59:59 GMT;当资源过期,且具有last-modified声明,则再次向服务器请求时会带上if-modified-since字段,跟资源的最后修改时间做对比,一致会被认为没有修改,不一致会返回新的资源 |
Etag | 根据资源的内容编码生成的一串唯一标识字符串。缓存验证,响应头中返回。Cache-Control设置为no-cache时会启用。 | 如:Etag: W/"78ecbd71fc13993de8d6859eb4d3a6ff";只要内容不同,就会生成不同的etag,启用etag后,再次请求该资源时,请求头会带if-no-match字段(值是之前的etag值),服务端根据if-no-match和服务器的etag做对比判断资源是否有更新。Etag的优先级高于Last-Modified |
3. 哪些资源会出现缓存
一是静态资源(js、css、image),二是 HTML 本身,都可能会被缓存。
3.1 如果资源已被缓存且没有过期,资源的 URL 没有发生变化 ,浏览器会先加载缓存的资源。如 html 中引用 a.js, 用户刷新页面时,a.js 会从浏览器缓存中获取,而不是从服务器中获取
3.2 如果 js、css、img 静态资源做了版本号处理,但 HTML 本身没有重新请求 ,也是会导致渲染原来的HTML页面
3.3 需要我们自动检测更新,并自动更新的场景(这个比较适用于场景2 和 场景3。因为场景1通常情况下我们不考虑):
- 场景1:用户开启着的浏览器,未刷新过页面;
- 场景2:我们提供给任意第三方的嵌入式 js 文件,如 webapp.js,第三方未知,且我们也无法修改第三方的 html 代码;
- 场景3:移动端 APP 嵌入到一级 NavBar 中的 H5 页面,为了用户体验设计,切换菜单时,并不销毁 webview,所以现象就是不杀掉 APP,H5页面不会刷新。
4. 针对3中提到的这些缓存,我们如何保证程序的更新呢
也从静态资源 和 HTML本身 两个方面来处理。
4.1 静态资源(js、css、img)的缓存处理方法
给引用的静态资源加版本号,版本号规则自行定义,只要保证升级时,版本号换成新的即可。示例如下:
备注:打包构建的前端工程通常 webpack、rollup 可以自动完成
- HTML中静态资源的引用修改文件名、加版本号 或 随机数
html
<script type="text/javascript" src="./scripts/webapp.js?v=3.7.329"></script>
<link rel="stylesheet" href="./styles/common.css?v=3.7.329">
<img src="./images/empty.png?v=3.7.329" width="100" height="100" />
- CSS中静态资源的引用修改文件名、加版本号 或 随机数
css
.icon-timer {
width: 10px;
height: 10px;
display: inline-block;
background-image: url(../images/timer.png?v=3.7.329);
background-repeat:no-repeat;
background-size:100% 100%;
-moz-background-size:100% 100%;
}
4.2 HTML本身的缓存处理方法(此部分如果能在配置的访问链接中加入 版本号,这里就不需要考虑了)
-
后端设置header头(对走后台程序的页面适用),如:
phpresponse.setDateHeader("Expries", -1); response.setHeader("Cache-Control", "no-cache"); response.setHeader("Pragma", "no-cache");
-
前端修改html,在head中添加meta信息(浏览器仍希望缓存的话无效,页面停留不刷新时无效)
html<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> <meta http-equiv="Cache" content="no-cache"> <meta http-equiv="Pragma" content="no-cache" /><!--禁止浏览器从本地计算机的缓存中访问页面的内容--> <meta http-equiv="Expires" content="0" />
-
前端使用 js 向 href 添加随机参数(注意:若为hash模式,则随机参数需要放置在 # 前)
javascriptif (!window.reloaded) { location.href += "?random=" + Date.now(); window.reloaded = true; } // 可以根据实际需要设置定时器后续自动清除 window.reloaded
延伸:如果对于独立部署的 html 文件,清除缓存的方法还有:
- 当 html 升级后,由服务器设置 指定html 文件的过期时间为当前时间(如 Linux 服务器中设置);
5. 自动检测更新方案
适用应用场景:3.3 中的 场景2和场景3
场景2:我们提供给任意第三方的嵌入式js文件,如 webapp.js,第三方未知,且我们也无法修改第三方的 html 代码;
场景3:移动端 APP 嵌入到一级 NavBar 中的 H5 页面,为了用户体验设计,切换菜单时,并不销毁 webview,所以现象就是不杀掉 APP,H5页面不会刷新。
5.1 方案一:WebSockets 或 Server-Sent Events 通过 WebSockets 实时通信,服务器将文件的更新通知到客户端,客户端收到更新通知,就可以下载新版本的 HTML 或 JS 文件;
缺点:需要在服务器部署 WebSocket 服务 或 SSE 服务
5.2 方案二:定时轮循(心跳检测) 前端设置一个定时器,定期向服务器发送请求,检测是否有新版本的 HTML 或 JS 文件,服务器把新版本的文件标志下发给前端。如果有新版本,前端可以提示用户更新或者自动下载新版本文件(定时器的间隔时间,根据实际场景设定,比如每天请求一次 或者 每一小时请求一次就够)
大致方案: 维护一个 version.json 文件,web app 构建时自动更新此文件,前端定时器定时向后台请求版本号信息,后台接口读取这个文件信息返回给定时器。如下图:
前后端需要做的事情:
- 前端:1. 构建工程时自动更新版本号文件; 2. 写定时器逻辑,定时发送请求获取版本号信息并存储,跟下一次请求到的版本号做比对,以判定是否有新文件,发现有新文件,就自动下载文件
- 后端:提供一个接口,从版本号文件中拿取版本信息返回给前端
5.3 方案三:监听页面可见时检测更新 此方案和 5.2 唯一不同的点,是把 5.2 的定时器改为 监听页面是否可见,页面可见时请求接口判断是否有新版本文件。
示例代码:
js
// 页面可见时,调用接口判断是否有新版本
function listenVersion() {
if (document.visibilityState === 'visible') {
// 代码版本信息如 {version: '1.0.0', lastmodified: '2023-10-10 15:15'}
let webappVerInfo = fetchVersion();
if(webappVerInfo.version !== localVerInfo.version){
// 版本有更新,更新本地版本信息,同时刷新当前页面获取新的资源
localVerInfo = webappVerInfo;
document.location.reload();
// 或 单独重新加载某个js资源
// var script = document.createElement('script');
// script.src = `webapp.js?v=${webappVerInfo.version}`;
// document.body.appendChild(script);
}
}else{
}
}
// 监听页面可见性变化,调用版本检测方法
document.addEventListener('visibilitychange', listenVersion);
// 记得页面卸载时销毁监听事件
// document.removeEventListener('visibilitychange', listenVersion)
visibilitychange 事件介绍:developer.mozilla.org/zh-CN/docs/... 兼容性:当 visibleStateState 属性的值转换为 hidden 时,Safari 不会按预期触发 visibilitychange;因此,在这种情况下,你还需要包含代码以侦听 pagehide 事件。 出于兼容性原因,请确保使用 document.addEventListener 而不是 window.addEventListener 来注册回调。Safari <14.0 仅支持前者。
6. 按天更新方案/定期更新
方案说明:
webapphook.js
(被 html 直接引用,此 js 中动态加载 webapp.js) + webapp.js
。 html 中引用 webapphook.js, 在 webapphook.js 中动态加载 webapp.js 并带上日期或日期时间戳,webapp.js 中为主要业务逻辑代码。
示例:
javascript
// index.html
<script type="text/javascript" src="./scripts/webapphook.js"></script>
// webapphook.js
let nowdate = new Date();
let nowversion = `${nowdate.getFullYear()}${nowdate.getMonth()+1}${nowdate.getDay()}`;
let webappjs = document.createElement('script');
webappjs.type='text/javascript';
webappjs.defer = true;
webappjs.async = true;
webappjs.src = `./webapp.js?v=${nowversion}`;
document.body.appendChild( webappjs );
// webapp.js
// web app 主代码
改变 nowversion
的逻辑,可以扩展为定期更新,比如当前时间+86400 等等..
文章参考: