Web项目减少资源加载失败白屏问题

背景

在做web项目的时候,比较常见的一个问题就是页面白屏,而页面白屏在我们的实际排查下来重点是在以下几个方面出了问题。

  • 网络问题:未能加载到资源或者资源加载不完整
  • 逻辑问题:代码报错

逻辑问题相对好排查,但是网络问题因为不同的用户,网络情况千差万别会有很多不确定性。这里介绍下我们在网络方面导致的白屏碰到的问题,以及对应的解决方案

问题

下面所聊的网络问题不考虑用户断网的情况,用户断网的情况要能访问需要配合缓存策略,但是不是这里讨论的重点。

cdn资源故障

我们经常碰到客诉说某个用户无法打开页面,按历史的经验我们大多情况分析下来就是用户网络访问不到我们的域名下的cdn资源,这里面可能有很多问题,比如用户dns解析不到正确的域名,比如当前用户所在地的cdn故障等等。总之就是资源无法正常访问到。

在以前碰到这种问题后我们是第一时间联系运维进行cdn厂商的切换,然后再通知用户重试,这种方法一是链路很长,需要用户配合,二是切换了cdn可能这个用户好了,其他用户出问题了。在这个情况下我们需要一种方式在资源加载失败的时候自动切换另外的备用cdn进行加载。

要达到这个效果可以参考如下的代码,这个代码会放到html的head标签里面,放到所有资源引用之前执行

js 复制代码
window.jsRetry = {
      oldAppendChild: document.head.appendChild,
      // 设置备用的域名,比如设置默认访问域名,再加上华为,火山等cdn厂商的域名
      domainGroup: [
        ['static.xxxx.com', 'static-hw.xxxx.com', 'static-hs.xxxx.com'],
        ['image.xxxx.com','imagecdn.xxxx.com', 'imagecdn-hw.xxxx.com', 'imagecdn-hs.xxxx.com']
      ],
      /**
       * 获取下一个备用域名
       */
      getNextUrl: function(src) {
        if (typeof src !== 'string' || src.length === 0) return ''
        var domainGroup = window.jsRetry.domainGroup
        for (var a = 0; a < domainGroup.length; a++) {
          for (var i = 0; i < domainGroup[a].length; i++) {
            if (src.indexOf('://' + domainGroup[a][i]) != -1) {
              var next = domainGroup[a][i + 1]
              return next ? src.replace('://' + domainGroup[a][i], '://' + next) : ''
            }
          }
        }
        return ''
      },
      /**
       * 对html里面依赖的js资源注册重试逻辑,
       * 对图片资源注入重试逻辑
       */
      handleGlobalError(event){
        if (!event || !event.target) return
        var tagName = event.target.tagName
        if (tagName === 'SCRIPT') {
          window.jsRetry.retryHtmlScript(event.target)
        }
        if (tagName === 'IMG') {
          window.jsRetry.retryImage(event.target)
        }
      },
      retryHtmlScript: function(node) {
        if (document.readyState !== 'loading') return
        if (node.tagName != 'SCRIPT' || node.onerror || node.onload || node.async) return
        var nextUrl = window.jsRetry.getNextUrl(node.src)
        if (nextUrl) {
          document.write('<script type="text/javascript" src="' + nextUrl + '"><\/script>')
        }
      },
      retryImage: function(node) {
        var originUrl = node.getAttribute('src')
        if (typeof originUrl != 'string' || !originUrl) return
        console.warn('image load fail:', node.src)
        var url = node.src
        // oss 私有桶,替换域名后无法访问
        // if (url.indexOf('Expires=') != -1) return
        // image 已经有错误处理逻辑就不统一处理
        if (node.onerror) return
        // 延迟替换,防止和别的替换逻辑冲突
        setTimeout(() => {
          var nextUrl = window.jsRetry.getNextUrl(node.src)
          if (nextUrl && node.src === url) {
            node.src = nextUrl
          }
        }, 600);
      },
      /**
       * 对通过webpack打包的项目,如果是chunk依赖的一些js加载失败,监控这些情况,并进行重试。
       */
      retryWebpackCSS: function(node) {
        if (!node.href || node.rel !== 'stylesheet' || node.type !== 'text/css' || !node.onload || !node.onerror) return window.jsRetry.oldAppendChild.apply(this, arguments)
        var nextUrl = window.jsRetry.getNextUrl(node.href)
        if (!nextUrl) return window.jsRetry.oldAppendChild.apply(this, arguments)
        var oldOnError = node.onerror
        var oldOnLoad = node.onload
        node.onerror = function (e) {
          var node2 = document.createElement('link')
          if (node.getAttributeNames) {
            var names = node.getAttributeNames()
            for (var i = 0; i < names.length; i++) {
              node2.setAttribute(names[i], node.getAttribute(names[i]))
            }
          }
          node2.href = nextUrl
          node2.rel = node.rel
          node2.type = node.type
          node2.onerror = oldOnError
          node2.onload = oldOnLoad
          document.head.appendChild(node2)
        }
        return window.jsRetry.oldAppendChild.apply(this, arguments)
      },
      retryWebpackJS: function(node) {
        // webpack 特征
        if (node.timeout !== 120) return window.jsRetry.oldAppendChild.apply(this, arguments)
        var nextUrl = window.jsRetry.getNextUrl(node.src)
        var oldOnError = node.onerror
        var oldOnLoad = node.onload
        if (!nextUrl) {
          node.onerror = function(event) {
            oldOnError.apply(this, arguments)
          }
          return window.jsRetry.oldAppendChild.apply(this, arguments)
        }
        node.onerror = function (e) {
          var node2 = document.createElement('script')
          if (node.getAttributeNames) {
            var names = node.getAttributeNames()
            for (var i = 0; i < names.length; i++) {
              node2.setAttribute(names[i], node.getAttribute(names[i]))
            }
          }
          node2.src = nextUrl
          node2.timeout = node.timeout
          node2.onerror = oldOnError
          node2.onload = oldOnLoad
          document.head.appendChild(node2)
        }
        return window.jsRetry.oldAppendChild.apply(this, arguments)
      }
    }
    
    // 基座 html script 重试
    window.addEventListener('error', window.jsRetry.handleGlobalError, true)
    // 代理替换掉 document.head.appendChild的方法,方便实现 webpack 打包的chunk加载失败后进行监控和重试。
    document.head.appendChild = function (node) {
      if (node && node.tagName === 'SCRIPT') return window.jsRetry.retryWebpackJS.apply(this, arguments)
      if (node && node.tagName === 'LINK') return window.jsRetry.retryWebpackCSS.apply(this, arguments)
      return window.jsRetry.oldAppendChild.apply(this, arguments)
    }

分析下代码,首先可以看到如下代码,资源加载我们主要有两类资源,一种是打包嵌入到html的资源引用,一部分是异步加载的一些chunk,这里面有webpack的chunk,也有自己业务逻辑动态插入的scrpit标签,下面的代码一方面是做全局的error监控,分析error里面是否是script或image加载失败,然后进行分开的处理, 一方面是代理掉 document.head.appendChild方法,记录动态插入的资源,在加载失败的时候进行重试。 原理本身不复杂,可以按自身业务进行修改。

js 复制代码
 // 基座 html script 重试
    window.addEventListener('error', window.jsRetry.handleGlobalError, true)
    // 代理替换掉 document.head.appendChild的方法,方便实现 webpack 打包的chunk加载失败后进行监控和重试。
    document.head.appendChild = function (node) {
      if (node && node.tagName === 'SCRIPT') return window.jsRetry.retryWebpackJS.apply(this, arguments)
      if (node && node.tagName === 'LINK') return window.jsRetry.retryWebpackCSS.apply(this, arguments)
      return window.jsRetry.oldAppendChild.apply(this, arguments)
    }

通过上述方法的嵌入后 当我们的页面加载 static.xxxx.com/abs.js 失败后,他会重新加载 static-hw.xxxx.com/abs.js ,如果这个加载失败后继续重试加载 static-hs.xxxx.com/abs.js .当三个加载失败后才会真正的失败。 通过这个方法,在用户访问不到某个域名下的cdn后可以做到自动在客户端进行切换。减少这种问题带来的客诉。

缓存不完整资源问题

在我们历史碰到的白屏问题中花了很多时间才发现的一个非常隐蔽的问题,这个问题也会导致白屏,并且很难排查,最终的解决方案都是清除缓存才能解决。这个问题就是 当访问一个资源 比如 static.xxxx.com/abs.js 。虽然状态码是200 正常访问了,但是实际获取到的js内容是被截断不完整的。 这种情况一般会报错,错误的关键字有 Unexpected EOF 或者 Unexpected end of script android 平台和ios有区别。如果js配置了强缓存,在这种被截断的资源被缓存到本地了,会导致该用户一直无法正常访问。所以我们需要在这种错误发生后清除该资源的缓存。

js 复制代码
window.JsIntegrityKey = "JsIntegrity";
 window.scriptHandle = function (event) {
      if (/Unexpected EOF|Unexpected end of script/.test(event.message) && event.filename && typeof fetch === 'function') {
       // 清除缓存的逻辑, 有次数的记录,防止一直在报错,一直在重新刷新, 如果是在app里面,可以通过自己的一些bridge调用app的清除缓存能力
        var url = event.filename || '';
        if (/^https?/.test(url)) {
          var store = {};
          try {
            store = JSON.parse(localStorage.getItem(window.JsIntegrityKey) || JSON.stringify({first: new Date().getTime()}));
          } catch(e) {};
          var count = store[url] || 0;
          if (count >= 10) return;
          // 通过 cache设置 reload 强制重新获取。 
          fetch(url, {
            "method": "GET",
            "cache": "reload"
          });
          store[url] = ++count;
          localStorage.setItem(window.JsIntegrityKey, JSON.stringify(store));
        }
      }
    };
    window.addEventListener('error', window.scriptHandle.bind(this), true);

总结

对于web项目的稳定性方面这里介绍了一些加强的方式,通过这些方式能比较好的解决一些端上网络的不确定性带来的问题,希望能帮到你

相关推荐
DaMu2 小时前
Cesium & Three.js 【移动端手游“户外大逃杀”】 还在“画页面的”前端开发小伙伴们,是时候该“在往前走一走”了!我们必须摆脱“画页面的”标签!
前端·gis
非专业程序员2 小时前
一文读懂Font文件
前端
Asort2 小时前
JavaScript 从零开始(七):函数编程入门——从定义到可重用代码的完整指南
前端·javascript
Johnny_FEer2 小时前
什么是 React 中的远程组件?
前端·react.js
我是日安2 小时前
从零到一打造 Vue3 响应式系统 Day 10 - 为何 Effect 会被指数级触发?
前端·vue.js
知了一笑2 小时前
「AI」网站模版,效果如何?
前端·后端·产品
艾小码2 小时前
用了这么久React,你真的搞懂useEffect了吗?
前端·javascript·react.js
知觉2 小时前
实现@imput支持用户输入最多三位整数,最多一位小数的数值
前端
RoyLin2 小时前
TypeScript设计模式:状态模式
前端·后端·typescript