Skip to content

脚本加载失败如何重试? ​

思路 ​

需要考虑两个问题:

  1. 什么时候感知到脚本加载失败?
  2. 如何安全地重新发起加载?

先考虑第一个问题,什么时间点重试?

html
<!-- 为每个 script 监听 onerror 事件 -->
<body>
  <script src="example1.js" onerror="handleError()"></script>
  <script src="example2.js" onerror="handleError()"></script>
  <script src="example3.js" onerror="handleError()"></script>
</body>
html
<head>
  <script>
    // 统一监听 error
    window.addEventListener(
      'error',
      (e) => {
        // 过滤非脚本加载错误,保证后续的逻辑只针对于脚本加载错误
        if (e instanceof ErrorEvent || e.target.tagName !== 'SCRIPT') {
          return
        }
        // 处理重试逻辑 ...
      },
      true, // 由于脚本的 error 事件不冒泡,需要使用捕获阶段
    )
  </script>
</head>

<body>
  <script src="example1.js"></script>
  <script src="example2.js"></script>
  <script src="example3.js"></script>
</body>

由于现代前端工程通常会产出多个入口脚本、动态脚本和第三方脚本,所以不建议依赖方案 1。手动在每个 <script> 上都写 onerror,维护成本较高。

方案2 通过监听全局 error 事件来捕获所有脚本加载失败的情况,更加通用和优雅。需要注意的是,脚本的 error 事件不会冒泡,所以需要在捕获阶段监听。


再考虑第二个问题,如何重试?

  1. 准备一份备用域名列表
  2. 记录每个脚本当前尝试的备用域名索引
  3. 当脚本加载失败时,构造新的 URL 并重新插入脚本节点

注意事项 ​

  • 不建议使用 document.write() 重新写入脚本。它会依赖文档写入时机,文档已完成解析后使用还有副作用。
  • 重试时最好保留原脚本的重要属性,例如 type、crossorigin、referrerpolicy。
  • 需要限制最大重试次数,避免无限循环。
  • 如果脚本带有 integrity,更换源站时还要考虑 SRI 校验是否依然成立。

完整示例 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Script Retry</title>
    <script>
      const backupHosts = ['cdn-a.example.com', 'cdn-b.example.com', 'cdn-c.example.com']
      const nextHostIndex = new Map()

      function cloneScriptWithNewSrc(oldScript, newSrc) {
        const newScript = document.createElement('script')
        for (const { name, value } of oldScript.attributes) {
          if (name !== 'src') {
            newScript.setAttribute(name, value)
          }
        }
        newScript.src = newSrc
        return newScript
      }

      window.addEventListener(
        'error',
        (event) => {
          if (event instanceof ErrorEvent || !(event.target instanceof HTMLScriptElement)) {
            return
          }

          const failedScript = event.target
          const url = new URL(failedScript.src)
          const key = url.pathname
          const currentIndex = nextHostIndex.get(key) ?? 0

          if (currentIndex >= backupHosts.length) {
            return
          }

          url.host = backupHosts[currentIndex]
          nextHostIndex.set(key, currentIndex + 1)

          const retryScript = cloneScriptWithNewSrc(failedScript, url.toString())
          failedScript.replaceWith(retryScript)
        },
        true,
      )
    </script>
  </head>
  <body>
    <script src="https://cdn.example.com/static/app.js" defer></script>
  </body>
</html>

基于 MIT 许可发布