表现:F12检查发现资源404错误,一般是app.{hash}.css, app.{hash}.js 等文件404。
原因:发版后静态资源名称变了,但浏览器缓存了旧的index.html,引用的还是旧的资源url,资源404,所以白屏了。
问题解决思路:在index.html写JS去检查是否资源载入出错,如果是则自动刷新。
因为script标签是编译后插入的,没法用onerror事件去检查,因此另想方案。
既然是script标签,那document.getElementByTagName是可以取到的。注意这在调试模式下不可行,因为调试模式只有一个app.js。
function checkUpgrade(){
var scriptEls = document.getElementsByTagName("script");
}
这样就可以取到script标签了,但是为什么只有一个?
这一个就是写的这段代码自身,因为浏览器js执行是单线程的,index.html刚解析和执行到这里,后面插入的script标签此时没有解析,因此我们需要判断一下延迟去处理。
if (scriptEls.length < 3) {
return setTimeout(checkUpgrade, 1000);
}
接下来就是构造一个ajax请求去检测文件是否存在了,因为我们不需要单独再去下载一次文件,这里用head请求就好了。
遍历scriptEls,判断是否是我们要检测的目标
if (element.src && element.src.indexOf('./static/js/')) {}
如果是就执行检测
var httpRequest_ = createHttpRequest();
httpRequest_.open("HEAD", element.src, true);
httpRequest_.send(null);
可以写个函数来看返回的结果
function handleResponse(xhr, callback) {
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
callback(xhr.status);
}
};
xhr.onerror = function () {
}
}
好了,基本上表明清楚了。
var httpRequest_ = createHttpRequest();
httpRequest_.open("HEAD", element.src, true);
httpRequest_.send(null);
handleResponse(httpRequest_, function (status) {
if (breakNext)
return;
if (status == 404) {
//这是版本已经更新了,客户端缓存的旧版
//刷新就好了
fixedLoadingText = '正在刷新,请稍后';
breakNext = 1;
setTimeout(() => location.reload(), 3 * 1000);
}
}
用breakNext是因为我们检测一个就好了,避免发很多不必要的请求
用fixedLoadingText是因为我做了一个加载动画,这个动画会在app.vue$mounted事件执行完毕后,也就是vue实例基本渲染好之后隐藏。
当然除了404,我们还可以做更多检测
if (status == 502 || status == 504 || status == 500) {
//这是后端无响应,提示正在升级
//10秒后继续检测是否升级完成
fixedLoadingText = '正在升级,请稍后';
breakNext = 1;
setTimeout(checkUpgrade, 5 * 1000);
}
else if (status == 0) {
//这是网络错误
// 一会儿再试
fixedLoadingText = '网络故障,正在重试';
breakNext = 1;
setTimeout(checkUpgrade, 10 * 1000);
}
奉上全部代码
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<link rel="shortcut icon" type=image/x-icon href=static/favicon.ico>
<title>VueApplication</title>
<style media=screen type=text/css>
#appLoading {
100%;
height: 100%;
vertical-align: middle;
}
#appLoading p {
background-color: white;
border: 2px solid #ccc;
text-align: center;
position: absolute;
display: block;
400px;
font-size: 20px;
padding: 20px;
top: 50%;
left: 50%;
-webkit-transform: translateY(-50%) translateX(-50%);
transform: translateY(-50%) translateX(-50%);
}
</style>
</head>
<body>
<div id=appLoading><span></span></div>
<div id=app style="display: none"></div>
<script>
(function () {
var dot = 1;
var dots = { 0: '', 1: '.', 2: '..', 3: '...' };
var checkError = true;
var loadingText = '载入中';
var fixedLoadingText = '';
var loadStart = new Date();
var percent = 0;
var breakNext = false;
var vueAppLoad = function (loadEnd) {
var loadElapsed = loadEnd - loadStart;
console.log("loadElapsed", loadElapsed / 1000, 's');
//关闭载入提示,显示app界面
document.getElementById("appLoading").style.display = "none";
document.getElementById("app").style.display = "block";
}
var updateLoadingText = function (loadingText) {
document.getElementById("appLoading").innerHTML = '<p><span style="float:left">' + loadingText + '</span><span style="float:right">' + percent + '%</span></p>';
}
var itv =
setInterval(() => {
dot = dot == 4 ? 0 : dot;
var loading = document.getElementById("appLoading");
var loadEnd = new Date();
if (percent == 100) {
clearInterval(itv);
checkError = false;
vueAppLoad(loadEnd);
return;
}
var loadSeconds = (loadEnd - loadStart) / 1000;
if (!fixedLoadingText) {
if (loadSeconds < 5) {
percent = parseInt(loadSeconds * 5);
}
if (loadSeconds > 10) {
loadingText = '请稍后'
percent = 50;
}
if (loadSeconds > 20) {
loadingText = '努力载入'
percent = 60
}
if (loadSeconds > 30) {
loadingText = '网有点慢'
percent = 70;
}
if (loadSeconds > 40) {
fixedLoadingText = '请检查网络'
percent = 80;
}
}
//检查app.vue事件创建的属性
if (window.appCreated) {
percent = 90;
}
if (window.appMounted) {
percent = 100;
}
updateLoadingText((fixedLoadingText || loadingText) + dots[dot]);
dot++;
}, 450);
function checkUpgrade() {
//reset vars
breakNext = false;
function createHttpRequest() {
if (window.ActiveXObject) {
return new ActiveXObject("Microsoft.XMLHTTP");
}
else if (window.XMLHttpRequest) {
return new XMLHttpRequest();
}
}
function handleResponse(xhr, callback) {
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
callback(xhr.status);
}
};
xhr.onerror = function () {
}
}
//版本更新后js和css的文件名称将更改,但因为客户端缓存了旧版的index.html页面
//因此载入静态资源会404,表现为白屏
//通过head请求去检测是否404来确认是否版本已更新,如果版本已更新则刷新页面
var scriptEls = document.getElementsByTagName("script");
if (scriptEls.length < 3) {
return setTimeout(checkUpgrade, 1000);
}
for (let i = 0; i < scriptEls.length; i++) {
if (breakNext)
break;
const element = scriptEls[i];
if (element.src && element.src.indexOf('./static/js/')) {
var httpRequest_ = createHttpRequest();
try {
httpRequest_.open("HEAD", element.src, true);
httpRequest_.send(null);
handleResponse(httpRequest_, function (status) {
if (breakNext)
return;
if (status == 404) {
//这是版本已经更新了,客户端缓存的旧版
//刷新就好了
fixedLoadingText = '正在刷新,请稍后';
breakNext = 1;
setTimeout(() => location.reload(), 3 * 1000);
}
else if (status == 502 || status == 504 || status == 500) {
//这是后端无响应,提示正在升级
//10秒后继续检测是否升级完成
fixedLoadingText = '正在升级,请稍后';
breakNext = 1;
setTimeout(checkUpgrade, 5 * 1000);
}
else if (status == 0) {
//这是网络错误
// 一会儿再试
fixedLoadingText = '网络故障,正在重试';
breakNext = 1;
setTimeout(checkUpgrade, 10 * 1000);
}
else {
//其他情况一般无需处理了
}
});
}
catch (ex) {
console.log(ex);
}
}
}
}
checkUpgrade();
})();
</script>
</body>
</html>
在app.vue的mounted事件中做个标记,这样index.html就可以用这个标记去检查vue是否渲染好了。
mounted() {
window.appMounted = new Date(); //通知index.html
},