脚本加载失败重试机制

概述

本文档介绍如何在网页中实现脚本加载失败自动切换到备用CDN的重试机制。通过监听全局error事件,当检测到脚本加载失败时,系统会自动尝试从备用CDN源重新加载脚本,直到成功或所有备用源耗尽。此实现方案能有效提高网站关键脚本的可用性,确保即使主CDN出现故障,用户仍然能正常使用网站功能。

实现原理

  1. 监听错误事件 :使用window.addEventListener('error')捕获页面错误
  2. 识别脚本错误:筛选出脚本加载失败的错误类型
  3. 重试机制:失败后切换到下一个备用CDN源
  4. 状态管理:跟踪当前尝试的CDN索引和重试状态

完整实现代码

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>脚本加载失败重试机制</title>
  <style>
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      line-height: 1.6;
      color: #333;
      max-width: 1000px;
      margin: 0 auto;
      padding: 20px;
      background-color: #f8f9fa;
    }

    .container {
      background-color: white;
      border-radius: 10px;
      box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
      padding: 30px;
      margin-top: 20px;
    }

    h1 {
      color: #2c3e50;
      text-align: center;
      border-bottom: 2px solid #3498db;
      padding-bottom: 15px;
    }

    h2 {
      color: #3498db;
      margin-top: 30px;
    }

    .status-panel {
      background-color: #e8f4fd;
      border-left: 4px solid #3498db;
      padding: 15px;
      margin: 20px 0;
      border-radius: 0 5px 5px 0;
    }

    .code-block {
      background-color: #2d2d2d;
      color: #f8f8f2;
      padding: 15px;
      border-radius: 5px;
      overflow-x: auto;
      margin: 20px 0;
      font-family: 'Consolas', monospace;
    }

    .success {
      color: #27ae60;
    }

    .warning {
      color: #f39c12;
    }

    .error {
      color: #e74c3c;
    }

    .btn {
      background-color: #3498db;
      color: white;
      border: none;
      padding: 10px 20px;
      border-radius: 5px;
      cursor: pointer;
      font-size: 16px;
      margin: 10px 5px;
      transition: background-color 0.3s;
    }

    .btn:hover {
      background-color: #2980b9;
    }

    .btn-test {
      background-color: #2ecc71;
    }

    .btn-test:hover {
      background-color: #27ae60;
    }

    .cdn-list {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      margin: 15px 0;
    }

    .cdn-item {
      background-color: #e8f4fd;
      padding: 8px 15px;
      border-radius: 20px;
      font-size: 14px;
    }

    .cdn-item.active {
      background-color: #3498db;
      color: white;
    }

    .cdn-item.failed {
      background-color: #fadbd8;
      color: #c0392b;
    }

    .log-container {
      background-color: #2d2d2d;
      color: #f8f8f2;
      padding: 15px;
      border-radius: 5px;
      height: 150px;
      overflow-y: auto;
      margin-top: 20px;
      font-family: 'Consolas', monospace;
      font-size: 14px;
    }

    .log-entry {
      margin: 5px 0;
    }
  </style>
</head>

<body>
  <div class="container">
    <h1>脚本加载失败重试机制</h1>

    <div class="status-panel">
      <h2>当前状态</h2>
      <p>脚本状态: <span id="scriptStatus">未加载</span></p>
      <p>CDN状态: <span id="cdnStatus">等待加载</span></p>
      <div class="cdn-list" id="cdnList"></div>
    </div>

    <h2>实现代码</h2>
    <div class="code-block">
      <pre><code>
      // CDN配置列表
      const CDN_SOURCES = [
          'https://cdn.primary.com/library.js',  // 主CDN
          'https://cdn.backup1.com/library.js',  // 备用CDN 1
          'https://cdn.backup2.com/library.js',  // 备用CDN 2
          'https://cdn.backup3.com/library.js'   // 备用CDN 3
      ];

      let currentCdnIndex = 0;
      let scriptLoaded = false;

      // 加载脚本函数
      function loadScript() {
          if (currentCdnIndex >= CDN_SOURCES.length) {
              logError('所有CDN源均加载失败,无可用CDN');
              return;
          }

          const scriptUrl = CDN_SOURCES[currentCdnIndex];
          logInfo(`尝试加载脚本: ${scriptUrl}`);
          updateCdnStatus(currentCdnIndex, 'loading');

          const script = document.createElement('script');
          script.src = scriptUrl;
          script.async = true;

          // 标记当前加载的CDN索引
          script.dataset.cdnIndex = currentCdnIndex;

          script.onload = () => {
              scriptLoaded = true;
              logSuccess(`成功加载脚本: ${scriptUrl}`);
              updateCdnStatus(currentCdnIndex, 'success');
              document.getElementById('scriptStatus').textContent = '已加载';
              document.getElementById('cdnStatus').textContent = `使用源: ${scriptUrl}`;
          };

          document.head.appendChild(script);
      }

      // 错误处理监听器
      window.addEventListener('error', (event) => {
          // 检查是否是脚本加载错误
          if (event.target.tagName === 'SCRIPT' && !scriptLoaded) {
              const cdnIndex = event.target.dataset.cdnIndex;

              // 验证错误来自当前处理的脚本
              if (cdnIndex && parseInt(cdnIndex) === currentCdnIndex) {
                  event.preventDefault();

                  logError(`CDN加载失败: ${CDN_SOURCES[currentCdnIndex]}`);
                  updateCdnStatus(currentCdnIndex, 'failed');

                  // 移除失败的脚本元素
                  event.target.remove();

                  // 尝试下一个CDN
                  currentCdnIndex++;
                  loadScript();
              }
          }
      }, true); // 使用捕获模式,因为错误不会冒泡

      // 初始加载
      loadScript();</code></pre>
    </div>

    <h2>机制说明</h2>
    <h3>工作流程</h3>
    <ol>
      <li>初始化时从主CDN加载脚本</li>
      <li>如果加载失败,触发全局错误事件</li>
      <li>错误处理器识别脚本加载失败</li>
      <li>移除失败的脚本元素</li>
      <li>切换到下一个备用CDN源</li>
      <li>重复此过程直到成功或所有CDN耗尽</li>
    </ol>

    <h3>注意事项</h3>
    <ul>
      <li>使用捕获模式(<code>true</code>作为第三个参数)监听错误事件,因为脚本错误不会冒泡</li>
      <li>必须验证错误来自当前处理的脚本,避免处理其他脚本错误</li>
      <li>每次重试前移除失败的脚本元素,防止内存泄漏</li>
      <li>使用<code>preventDefault()</code>阻止浏览器默认错误处理</li>
      <li>确保CDN列表中的URL顺序正确(主CDN优先)</li>
    </ul>

    <h2>测试控制台</h2>
    <div>
      <button id="testSuccess" class="btn btn-test">模拟成功加载</button>
      <button id="testFailure" class="btn">模拟加载失败</button>
      <button id="reset" class="btn">重置状态</button>
    </div>

    <h3>操作日志</h3>
    <div class="log-container" id="logContainer"></div>
  </div>

  <script>
    // 以下是实际实现代码
    // CDN配置列表
    const CDN_SOURCES = [
      'https://cdn.primary.com/library.js',  // 主CDN
      'https://cdn.backup1.com/library.js',  // 备用CDN 1
      'https://cdn.backup2.com/library.js',  // 备用CDN 2
      'https://cdn.backup3.com/library.js'   // 备用CDN 3
    ];

    let currentCdnIndex = 0;
    let scriptLoaded = false;

    // 初始化CDN列表显示
    function initCdnList() {
      const cdnList = document.getElementById('cdnList');
      cdnList.innerHTML = '';

      CDN_SOURCES.forEach((url, index) => {
        const cdnItem = document.createElement('div');
        cdnItem.className = 'cdn-item';
        cdnItem.textContent = `CDN ${index + 1}`;
        cdnItem.id = `cdn-${index}`;
        cdnList.appendChild(cdnItem);
      });
    }

    // 更新CDN状态显示
    function updateCdnStatus(index, status) {
      const cdnElement = document.getElementById(`cdn-${index}`);
      if (!cdnElement) return;

      // 清除所有状态类
      cdnElement.classList.remove('active', 'failed');

      if (status === 'loading') {
        cdnElement.classList.add('active');
        cdnElement.innerHTML = `CDN ${index + 1} <span class="warning">(加载中...)</span>`;
      } else if (status === 'success') {
        cdnElement.classList.add('active');
        cdnElement.innerHTML = `CDN ${index + 1} <span class="success">(成功)</span>`;
      } else if (status === 'failed') {
        cdnElement.classList.add('failed');
        cdnElement.innerHTML = `CDN ${index + 1} <span class="error">(失败)</span>`;
      }
    }

    // 日志函数
    function logInfo(message) {
      addLogEntry(message, 'info');
    }

    function logSuccess(message) {
      addLogEntry(message, 'success');
    }

    function logError(message) {
      addLogEntry(message, 'error');
    }

    function addLogEntry(message, type) {
      const logContainer = document.getElementById('logContainer');
      const logEntry = document.createElement('div');
      logEntry.className = 'log-entry';

      const timestamp = new Date().toLocaleTimeString();

      switch (type) {
        case 'success':
          logEntry.innerHTML = `<span class="success">[${timestamp}] ✓ ${message}</span>`;
          break;
        case 'error':
          logEntry.innerHTML = `<span class="error">[${timestamp}] ✗ ${message}</span>`;
          break;
        default:
          logEntry.innerHTML = `<span>[${timestamp}] ➜ ${message}</span>`;
      }

      logContainer.appendChild(logEntry);
      logContainer.scrollTop = logContainer.scrollHeight;
    }

    // 加载脚本函数
    function loadScript() {
      if (currentCdnIndex >= CDN_SOURCES.length) {
        logError('所有CDN源均加载失败,无可用CDN');
        document.getElementById('cdnStatus').textContent = '所有CDN加载失败';
        return;
      }

      const scriptUrl = CDN_SOURCES[currentCdnIndex];
      logInfo(`尝试加载脚本: ${scriptUrl}`);
      updateCdnStatus(currentCdnIndex, 'loading');

      const script = document.createElement('script');
      script.src = scriptUrl;
      script.async = true;

      // 标记当前加载的CDN索引
      script.dataset.cdnIndex = currentCdnIndex;

      script.onload = () => {
        scriptLoaded = true;
        logSuccess(`成功加载脚本: ${scriptUrl}`);
        updateCdnStatus(currentCdnIndex, 'success');
        document.getElementById('scriptStatus').textContent = '已加载';
        document.getElementById('cdnStatus').textContent = `使用源: CDN ${currentCdnIndex + 1}`;
      };

      script.onerror = () => {
        // 这里不需要额外处理,因为error事件会被全局监听器捕获
      };

      document.head.appendChild(script);
    }

    // 错误处理监听器
    window.addEventListener('error', (event) => {
      // 检查是否是脚本加载错误
      if (event.target.tagName === 'SCRIPT' && !scriptLoaded) {
        const cdnIndex = event.target.dataset.cdnIndex;

        // 验证错误来自当前处理的脚本
        if (cdnIndex && parseInt(cdnIndex) === currentCdnIndex) {
          event.preventDefault();

          logError(`CDN加载失败: ${CDN_SOURCES[currentCdnIndex]}`);
          updateCdnStatus(currentCdnIndex, 'failed');

          // 移除失败的脚本元素
          event.target.remove();

          // 尝试下一个CDN
          currentCdnIndex++;
          loadScript();
        }
      }
    }, true); // 使用捕获模式,因为错误不会冒泡

    // 初始加载
    document.addEventListener('DOMContentLoaded', () => {
      initCdnList();
      loadScript();

      // 测试按钮
      document.getElementById('testSuccess').addEventListener('click', () => {
        scriptLoaded = true;
        logSuccess('测试: 脚本加载成功');
        document.getElementById('scriptStatus').textContent = '已加载(测试)';
        document.getElementById('cdnStatus').textContent = '测试成功状态';
        updateCdnStatus(currentCdnIndex, 'success');
      });

      document.getElementById('testFailure').addEventListener('click', () => {
        if (scriptLoaded) return;

        logError('测试: 模拟脚本加载失败');
        updateCdnStatus(currentCdnIndex, 'failed');

        // 模拟切换到下一个CDN
        currentCdnIndex++;
        loadScript();
      });

      document.getElementById('reset').addEventListener('click', () => {
        currentCdnIndex = 0;
        scriptLoaded = false;
        document.getElementById('logContainer').innerHTML = '';
        document.getElementById('scriptStatus').textContent = '未加载';
        document.getElementById('cdnStatus').textContent = '重置状态';
        initCdnList();
        loadScript();
      });
    });
  </script>
</body>

</html>

关键实现细节

1. CDN源配置

js 复制代码
const CDN_SOURCES = [
    'https://cdn.primary.com/library.js',  // 主CDN
    'https://cdn.backup1.com/library.js',  // 备用CDN 1
    'https://cdn.backup2.com/library.js',  // 备用CDN 2
    'https://cdn.backup3.com/library.js'   // 备用CDN 3
];
  • 按优先级顺序配置CDN源
  • 主CDN放在数组首位

2. 错误事件监听

js 复制代码
window.addEventListener('error', (event) => {
    if (event.target.tagName === 'SCRIPT' && !scriptLoaded) {
        // 处理脚本加载错误
    }
}, true); // 关键:使用捕获模式

3. 脚本加载与重试逻辑

js 复制代码
function loadScript() {
    // 检查是否还有可用CDN
    if (currentCdnIndex >= CDN_SOURCES.length) {
        logError('所有CDN源均加载失败,无可用CDN');
        return;
    }

    const script = document.createElement('script');
    script.src = CDN_SOURCES[currentCdnIndex];
    script.dataset.cdnIndex = currentCdnIndex; // 标记当前CDN索引
    
    document.head.appendChild(script);
}

4. 错误处理流程

js 复制代码
window.addEventListener('error', (event) => {
    event.preventDefault(); // 阻止默认错误处理
    
    // 记录错误
    logError(`CDN加载失败: ${CDN_SOURCES[currentCdnIndex]}`);
    
    // 移除失败的脚本
    event.target.remove();
    
    // 切换到下一个CDN
    currentCdnIndex++;
    loadScript();
}, true);

使用建议

  1. CDN选择策略

    • 将最可靠的CDN放在列表首位
    • 至少配置2-3个备用CDN源
    • 考虑使用不同提供商的CDN以增加冗余
  2. 性能考虑

    • 重试机制会增加额外加载时间
    • 设置合理的超时时间(本例未展示,实际可添加)
    • 避免过多重试次数(一般3-4次为宜)
  3. 错误处理

    • 当所有CDN都失败时,提供友好的用户提示
    • 考虑降级方案或本地备用资源
    • 记录错误信息以便后期分析
  4. 安全考虑

    • 确保所有CDN源都是可信任的
    • 使用HTTPS协议加载资源
    • 考虑添加完整性校验(SRI)
相关推荐
timeweaver4 小时前
深度解析 Nginx 前端 location 配置与优先级:你真的用对了吗?
前端·nginx·前端工程化
前端小白19958 小时前
面试取经:工程化篇-webpack性能优化之优化loader性能
前端·面试·前端工程化
前端小白19958 小时前
面试取经:工程化篇-webpack性能优化之减少模块解析
前端·面试·前端工程化
Linsk1 天前
当我把前端条件加载做到极致
前端·前端工程化
breeze_whisper4 天前
浏览器兼容性有何解?plugin-legacy
vite·前端工程化
阿里云视频云5 天前
实战揭秘|魔搭社区 + 阿里云边缘云 ENS,快速部署大模型的落地实践
云计算·边缘计算·cdn
努力的小雨7 天前
EdgeOne发放免费CDN套餐,靠谱嘛?
cdn
前端AK君8 天前
rolldown-vite初体验
前端·前端工程化
真正的粉丝8 天前
全面解析 ES Module 模块化
面试·前端工程化