1、CDN资源失效导致白屏
用户网络环境复杂,运营商劫持,国外访问CDN无效等等。这种情况下会导致白屏。
应得这种情况,识别资源加载识别,切换线路重新加载资源自然成了我们选择的方案。
大家已经有了一些方案:
configuring webpack public path at runtime
这些方案有通用的策略,也有针对webpack打包的针对性方案。不过这些方案总结来说,还是要实现2个大的功能:
- 识别资源加载失败
- 重新加载失败的资源
识别资源加载失败
对于第一个问题,上面头条的同学给一个简单通用的方案,就是这个:
<script>
var errors = [];
window.addEventListener('error', function (e) {
if (!(e instanceof ErrorEvent)) {
errors.push(e);
}
}, true);
</script>
利用addEventListener在捕获阶段获得错误,通过判断e不是ErrorEvent来判断是资源加载错误。但是这个错误不能判断资源是404还是网络问题,都统一返回一个error。如果需要知道具体的问题,还需要通过ajax请求一次这个出错的资源,才能知道具体问题(是404、域名解析不了、服务不可用、还是加载超时等)
捕获到错误以后,应该如何加载资源呢,重新加载哪些资源呢?js、css、image?
重新加载失败的资源
这个问题可以利用策略来解决。这里有两种策略:
- 页面内重新加载,在页面里获得当前页面失败到或者所有到资源,重新切换线路进行加载。
- 提示用户资源加载失败,然后reload页面,在url上给一个标示,让这个重新reload的页面采用新的资源路径。
第二种策略耗时会更久,这里就不讨论了,具体在业务中笔者也没有真的实现。我们来聊一聊页面内重新加载的思路。
上面头条的方案中,讨论了js重新加载的方案,这里我们会覆盖讨论css和背景图片的问题。
css资源和js资源
css资源和js资源都是通过dom标签加载的,所以实现方案上可以统一,流程都是识别所有的dom,然后把对应链接里的CDN域名提高为新的域名,再把得到的新链接生成dom重新插入到页面中,利用浏览器的并行加载,顺序执行重新执行一遍这些资源。
Tips: 如果出现部分js资源失败,部分成功。重新加载全部资源,可能会导致某些js执行出错,如果这些js文件不支持多次执行的话。我们这里不对这种情况做讨论,指考虑统一失败的情况,加载所有资源都使用了相同的CDN,且这些js都支持多次重复执行。
看如下代码:
function cndReloadAll (nodeName, urlAttr) {
var domNodeList = document.getElementsByTagName(nodeName)
for (var i = 0; i < domNodeList.length; i++) {
var onedom = domNodeList[i];
if (!onedom[urlAttr] || (onedom[urlAttr].indexOf('a.cdn.com'))) {
continue;
}
var newNode = document.createElement(nodeName);
for (var key in onedom.attributes) {
if (onedom.attributes.hasOwnProperty(key)) {
var nodeAttr = onedom.attributes[key]
var name = nodeAttr.name
if (nodeAttr.name === 'crossorigin') {
newNode.crossOrigin = 'crossorigin'
} else if (['onerror', 'onload', 'onabort'].indexOf(name) === -1) {
newNode[nodeAttr.name] = nodeAttr.value
}
}
}
newNode[urlAttr] = onedom[urlAttr].replace('a.cdn.com', 'b.cdn.com')
onedom.parentNode.insertBefore(newNode, onedom)
onedom.parentNode.removeChild(onedom)
}
}
代码里的函数cdnReloadAll有两个参数,nodeName表示标签名字,比如script或者link,urlAttr表示需要替换的属性,script对应src,link对应href。
a.cdn.com表示当前的CDN域名,b.cdn.com表示切换后的域名。这段代码还贴心的,复制了原来script标签里一些常用属性。
背景图片
把css文件上传到多个CDN以后,css文件里的背景图URL可能还是同一个,切换的时候需要同时进行切换。看如下代码:
function reloadBackgroundImage () {
var isSupportStyleSheets = document.styleSheets && document.styleSheets[0]
if (!isSupportStyleSheets) return false
var styleSheetsLength = document.styleSheets.length
for (var i = 0; i < styleSheetsLength; i++) {
var styleSheets = document.styleSheets[i]
try {
if (!(styleSheets.rules && styleSheets.rules.length > 0)) {
continue
}
for (var j = 0; j < styleSheets.rules.length; j++) {
var item = styleSheets.rules[j]
// is back null
var backStr = item.selectorText && item.style && item.style.backgroundImage
if (!backStr) {
continue
}
// is has real url
var matchRes = backStr.match(/url\(["|'](.*)["|']\)/)
var backgroundImage = matchRes && matchRes[1]
if (!backgroundImage) {
continue
}
// base64 not replace
if (backgroundImage.indexOf('data:image/') !== -1) {
continue;
}
var nodeName = "BACKGROUNDIMAGE"
if (backgroundImage.indexOf('a.cdn.com') === -1) {
continue;
}
var newUrl = backgroundImage.replace('a.cdn.com', 'b.cdn.com')
if (!newUrl) {
continue
}
var cssText = item.selectorText + "{ background-image: url(" + newUrl + ")}"
var cssTextKey = i + cssText
if (window.reloadCache[nodeName].indexOf(cssTextKey) === -1) {
window.reloadCache[nodeName].push(cssTextKey)
styleSheets.insertRule(cssText, styleSheets.rules.length)
}
}
} catch (err) {
throw Error("CdnAssetsSwitch replaceBackGroundImage " + err)
}
}
}
上面这段代码遍历了页面里所有的样式表,并且提取里面的background-image属性,如果发现是a.cdn.com的,就生成一个b.cdn.com的新样式插入到这个样式表中。里面还做了一个缓存,稍微提高一下性能。
这样我们就把重新加载资源搞定了。第一步和第二部要如何联合在一期呢,在不侵入代码的情况下。我们可以在<body>后,业务js代码前插入一段js代码,如下:
window.addEventListener('error', function (e) {
if (!(e instanceof ErrorEvent)) {
if (!isCdnError) {
isCdnError = true
var script = window.document.createElement('script');
script.type = 'text/javascript';
script.addEventListener('error', function () {
// TODO 继续切换其他线路
})
script.src = 'reload.js';
window.document.body.appendChild(script);
}
}
}, true)
其中reload.js里写的是上面重载css、js、和背景图片的代码,定义两个函数,并且执行。
未完待续~