1. 背景
移动端页面更新内容的过程中,经常会出现静态资源不更新,导致页面部分功能不可用或白屏的情况,本文档的方案就是为了解决这一情况。
2. 原因
部分手机和应用的浏览器会对网页资源进行缓存,对于单页面应用来说一旦入口资源被缓存,会导致应用无法更新。并且由于服务端更新应用时,采取的是覆盖更新的方式,旧版本的前端资源会被删除。这样当用户访问旧的入口文件时,部分未被缓存的资源会无法加载,导致页面白屏。
3. 如何确认页面静态资源是从缓存获取的
可以在devtools的console中执行
performance.getEntriesByType("navigation")[0].transferSize
这是浏览器自带的监听属性,chrome、火狐和较新版本Safari浏览器都支持
当transferSize返回的数值是0或300时,表示是从缓存获取的静态资源,否则表示资源是从网络请求过来的
transferSize属性的规范:www.w3.org/TR/resource…
transferSize属性的浏览器支持:developer.mozilla.org/en-US/docs/…
4. 方案
4.1 前端层面解决入口文件被缓存问题
4.1.1 临时方案
手机设置中清除浏览器或应用的缓存,此方案需要客户的配合,在耗费客成、产品大量精力的同时,也会降低应用的体验,因此这种方案只能作为保底方案。
4.1.2 通用方案
- 入口 html的响应头cache-control设置成no-cache,确保不使用缓存策略。
- 资源使用hash,且每次上线时给开发者中心的入口地址,添加版本号参数,每次前端包升级时都修改该参数,从而使端上缓存失效。
- 项目内的跳转链接需要带上时间戳参数,而且时间戳参数需要放在锚点之前,否则跳转后依然可能命中缓存,导致应用使用异常。
- 如果上面三条还不能解决问题,则将cache-control设置成no-store,彻底不使用缓存就可以解决问题。
- 前三条都做了还不起作用,且不想使用no-store的话,可以在入口html中,向window注入一个版本号,然后再获取url上带过来的版本号,比对两个版本号,如果不一致就强刷当前页面。需要注意使用sessionStorage存储一下刷新状态,否则两个版本号对不上时会持续触发刷新动作,导致应用无法使用。 示例代码如下:
window.appVersion = '1.0.2';
function getQueryParams (key) {
const query = location.search.substring(1);
const vars = query.split('&');
for (let i = 0; i < vars.length; i++) {
const pair = vars[i].split('=');
if (pair[0] === key) { return pair[1]; }
}
return '';
}
var pathVersion = getQueryParams('version');
var isRefreshed = window.sessionStorage.getItem('isRefreshed');
console.log('pathVersion: ', pathVersion);
console.log('appVersion: ', window.appVersion);
console.log('transferSize: ', performance.getEntriesByType("navigation")[0].transferSize);
if (pathVersion && pathVersion !== window.appVersion && !isRefreshed) {
window.sessionStorage.setItem('isRefreshed', true);
location.reload(true);
}
注意:
这个方法唯一比较麻烦的就是每次更新都需要改一下入口html中的appVersion,不改的话,每次进入应用检查版本号对不上就会强刷一次页面。
4.1.3 风险
通用方案中最后一条提到的页面强刷是一个风险性较高的操作,虽然增加了sessionStorage判断,但一旦出现死循环,会直接导致应用不可用甚至手机卡死的情况。因此需要在不同机型手机上多次验证,并在影响较小的项目中先尝试使用。
4.2 前端页面变更静态资源名字
页面在每次更新内容后,需要将html加载的js/css等名称进行变更,确保不触发变更资源的缓存,可以手动修改入口页面的文件加载名称,也可以通过打包工具动态的改变css/js等资源名字中的hash值,总之确保内容变更后名字也变更就行。
4.3 服务端层面入手解决
上面都是一些基于前端层面的补救措施,治标不治本,真正想要解决页面缓存问题,需要从服务端或代理服务器入手
4.3.1 强缓存
强缓存会设置一个过期时间,电脑本地时间距离上一次访问没超出这个过期时间,就会直接走缓存,很不灵活,不推荐使用。
强缓存这个过期时间可以通过前端页面在head中设置,比如:
<meta http-equiv="Cache-Control" content="max-age=31536000">
<meta http-equiv="Expires" content="Mon, 20 Oct 2023 00:00:00 GMT">
也可以通过服务端设置response的max-age,服务端或代理服务器若设置了强缓存,就会导致浏览器接收到的是强缓存资源。
4.3.2 协商缓存
很多项目现在常使用的是协商缓存,这种缓存更灵活,静态资源变更时,对应的资源标识会变,如果不变就会走304协商缓存,如果变了就会走200重新拉取资源。
http请求中request的If-Modified-Since/If-None-Match,就是上一次服务端给浏览器的资源标识,浏览器再次请求服务端时,会比对与response的Last-Modifed/Etag是否一致,一致的话就会走304,不一致就会走200重新拉取,因此每一次静态资源更新(比如css/js/图片等),其服务器返回的Last-Modifed或Etag都应该变更才对,否则就不会触发浏览器的重新拉取,而是会走缓存。
js或css变更后,触发协商缓存时,发现拿到的还是老的js或css,往往是因为html内容没有联动js/css一起变化,导致html对应的etag或Last-Modifed未变更,浏览器拿到的html还是老的,导致请求的js/css资源也是老的。
要解决这个问题,有两种方式:
- 在html中放一个可随打包变动的变量,每次前端打包时,随js/css内容或时间戳变更这个变量,即可保证html内容随之联动改变,这样html资源的etag就会改变,触发协商缓存时,就会返回新的html资源,js/css资源也就会拿到最新的。
- Html资源也可不进行任何缓存,确保用户每次拉到的html都是最新的,服务端需要返回html资源时,在response中去掉Last-Modifed和Etag,并给Expires一个很老的时间,确保html资源既不会走强缓存也不会走协商缓存。