前段时间转载了一篇文章《前端实现自动切换CDN,让资源高可用》,当时留了一个小尾巴,非 JavaScript 内容(例如图片等静态资源)如何实现高可用:
现在找到的解决方案也许能解决问题:
本文转载自前端白屏问题分析——CDN资源失效、JS代码不兼容、css代码不兼容_魔侠的沙滩-CSDN博客,遵循 CC 4.0 BY-SA 版权协议规范转载。
识别资源加载失败
对于第一个问题,上面头条的同学给一个简单通用的方案,就是这个:
1 2 3 4 5 6 7 8
| <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都支持多次重复执行。
看如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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可能还是同一个,切换的时候需要同时进行切换。看如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| 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] var backStr = item.selectorText && item.style && item.style.backgroundImage if (!backStr) { continue } var matchRes = backStr.match(/url\(["|'](.*)["|']\)/) var backgroundImage = matchRes && matchRes[1] if (!backgroundImage) { continue } 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的新样式插入到这个样式表中。里面还做了一个缓存,稍微提高一下性能。
这样我们就把重新加载资源搞定了。第一步和第二部要如何联合在一期呢,在不侵入代码的情况下。我们可以在
后,业务js代码前插入一段js代码,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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 () { }) script.src = 'reload.js'; window.document.body.appendChild(script); } } }, true)
|
其中reload.js里写的是上面重载css、js、和背景图片的代码,定义两个函数,并且执行。