前端白屏问题分析——CDN资源失效、JS代码不兼容、css代码不兼容

1、CDN资源失效导致白屏

用户网络环境复杂,运营商劫持,国外访问CDN无效等等。这种情况下会导致白屏。

应得这种情况,识别资源加载识别,切换线路重新加载资源自然成了我们选择的方案。

大家已经有了一些方案:

简单策略让前端资源实现高可用

域名劫持资源重加载方案

前端资源加载重试

configuring webpack public path at runtime

这些方案有通用的策略,也有针对webpack打包的针对性方案。不过这些方案总结来说,还是要实现2个大的功能:

  1. 识别资源加载失败
  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?

重新加载失败的资源

这个问题可以利用策略来解决。这里有两种策略:

  1. 页面内重新加载,在页面里获得当前页面失败到或者所有到资源,重新切换线路进行加载。
  2. 提示用户资源加载失败,然后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、和背景图片的代码,定义两个函数,并且执行。

未完待续~ 

发布了24 篇原创文章 · 获赞 3 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/u012787757/article/details/100180651
今日推荐