VUE发版白屏问题解决
表现: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 { width: 100%; height: 100%; vertical-align: middle; } #appLoading p { background-color: white; border: 2px solid #ccc; text-align: center; position: absolute; display: block; width: 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 },