前端资源加载失败?一个优雅的 JS 重试方案

75 阅读2分钟

前言

在实际生产环境中,我们经常会遇到 JavaScript 文件加载失败的情况:CDN 挂了、网络波动、文件路径错误等。这时候,一个优雅的重试机制就显得尤为重要。

本文将介绍一种基于 window.error 事件的 JS 资源加载失败自动重试方案,并深入分析其实现原理和应用场景。

业务场景

假设你的项目部署在多个 CDN 节点上,或者有多个备用资源路径:

  • 主 CDN:https://cdn1.example.com/script.js
  • 备用 CDN:https://cdn2.example.com/script.js
  • 本地备份:/local/script.js

当主 CDN 挂掉时,我们希望能自动切换到备用资源,而不是让页面直接报错。

核心实现

1. 基础版本:异步加载重试

const domains = ['cdn1.js', 'cdn2.js', 'local.js'];
const retry = {};

window.addEventListener('error', (event) => {
    // 只处理 script 标签的加载错误
    if (event.target.tagName !== 'SCRIPT' || event instanceof ErrorEvent) return;
    
    const originalSrc = event.target.src;
    const key = originalSrc;
    
    // 初始化重试计数
    if (!(key in retry)) {
        retry[key] = 0;
    }
    
    const index = retry[key];
    
    // 超过重试次数,放弃
    if (index >= domains.length) {
        console.error(`所有备用路径都失败: ${originalSrc}`);
        return;
    }
    
    const domain = domains[index];
    console.log(`尝试备用路径: ${domain}`);
    
    // 创建新的 script 标签
    const script = document.createElement('script');
    script.src = domain;
    document.body.insertBefore(script, event.target);
    
    retry[key]++;
}, true);

优点

  • 不阻塞页面渲染
  • 异步加载,性能好

缺点

  • 无法保证脚本执行顺序
  • 如果后续脚本依赖前面的脚本,会出问题

2. 进阶版本:同步加载重试(document.write)

const domains = ['1.js', '2.js', 'script2.js', '3.js'];
const retry = {};

window.addEventListener('error', (event) => {
    if (event.target.tagName !== 'SCRIPT' || event instanceof ErrorEvent) return;
    
    const originalSrc = event.target.src;
    const key = originalSrc;
    
    if (!(key in retry)) {
        retry[key] = 0;
    }
    
    const index = retry[key];
    if (index >= domains.length) {
        return;
    }
    
    const domain = domains[index];
    
    // 使用 document.write 实现同步加载
    document.write(`<script src="${domain}"><\/script>`);
    
    retry[key]++;
}, true);

优点

  • 阻塞式加载,保证执行顺序
  • 适合有依赖关系的脚本

缺点

  • 会阻塞页面渲染
  • 只能在页面加载期间使用,页面加载完成后使用会清空整个页面

关键技术点解析

1. 为什么使用捕获阶段?

window.addEventListener('error', handler, true);  // 第三个参数为 true

因为 error 事件不会冒泡,只能在捕获阶段捕获。这是与其他 DOM 事件的重要区别。

2. 如何区分脚本加载错误和运行时错误?

if (event.target.tagName !== 'SCRIPT' || event instanceof ErrorEvent) return;
  • event.target.tagName === 'SCRIPT':确保是 script 标签触发的错误
  • !(event instanceof ErrorEvent):排除 JavaScript 运行时错误

3. document.write vs createElement

方式加载方式执行顺序适用场景
document.write同步严格按顺序有依赖关系的脚本
createElement异步不保证顺序独立的脚本模块

4. 重试计数器的设计

const retry = {};
const key = originalSrc;  // 使用完整 URL 作为 key

if (!(key in retry)) {
    retry[key] = 0;
}

使用对象存储每个资源的重试次数,避免不同资源之间的重试计数互相干扰。

实际应用场景

场景一:CDN 容灾

const domains = [
    'https://cdn1.example.com/app.js',
    'https://cdn2.example.com/app.js',
    'https://cdn3.example.com/app.js',
    '/static/app.js'  // 本地备份
];

场景二:动态路径修正

window.addEventListener('error', (event) => {
    if (event.target.tagName !== 'SCRIPT') return;
    
    const url = new URL(event.target.src);
    const filename = url.pathname.split('/').pop();
    
    // 尝试从根路径加载
    const script = document.createElement('script');
    script.src = `/${filename}`;
    document.body.appendChild(script);
}, true);

场景三:版本回退

const versions = ['v2.0.0', 'v1.9.0', 'v1.8.0'];
let versionIndex = 0;

window.addEventListener('error', (event) => {
    if (event.target.tagName !== 'SCRIPT') return;
    
    if (versionIndex >= versions.length) return;
    
    const version = versions[versionIndex];
    const script = document.createElement('script');
    script.src = `https://cdn.example.com/${version}/app.js`;
    document.body.appendChild(script);
    
    versionIndex++;
}, true);

注意事项与最佳实践

1. 避免无限重试

const MAX_RETRIES = 3;

if (index >= MAX_RETRIES) {
    console.error('重试次数已达上限');
    // 上报监控系统
    reportError(originalSrc);
    return;
}

2. 添加监控上报

window.addEventListener('error', (event) => {
    if (event.target.tagName !== 'SCRIPT') return;
    
    // 上报到监控系统
    fetch('/api/log', {
        method: 'POST',
        body: JSON.stringify({
            type: 'script_load_error',
            src: event.target.src,
            time: Date.now()
        })
    });
    
    // 执行重试逻辑...
}, true);

3. 考虑跨域问题

如果备用资源在不同域名下,需要确保:

  • 设置正确的 CORS 头
  • 考虑使用 crossorigin 属性
<script src="https://cdn.example.com/app.js" crossorigin="anonymous"></script>

4. document.write 的使用限制

⚠️ 重要提醒document.write 只能在页面加载期间使用!

// ❌ 错误:页面加载完成后使用会清空页面
window.addEventListener('load', () => {
    document.write('<script src="app.js"></script>');  // 危险!
});

// ✅ 正确:在页面加载期间使用
window.addEventListener('error', (event) => {
    // 页面加载期间可以安全使用
    document.write('<script src="backup.js"></script>');
}, true);

性能优化建议

1. 预加载关键资源

<link rel="preload" href="critical.js" as="script">

2. 使用 Service Worker 缓存

self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request)
            .then(response => response || fetch(event.request))
    );
});

3. 资源优先级控制

const script = document.createElement('script');
script.src = 'app.js';
script.async = false;  // 保持执行顺序
script.defer = true;   // 延迟执行

完整示例代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>JS 资源加载重试示例</title>
    
    <script>
        const domains = ['cdn1.js', 'cdn2.js', 'local.js'];
        const retry = {};
        const MAX_RETRIES = 3;
        
        window.addEventListener('error', (event) => {
            // 只处理 script 加载错误
            if (event.target.tagName !== 'SCRIPT' || event instanceof ErrorEvent) {
                return;
            }
            
            const originalSrc = event.target.src;
            const key = originalSrc;
            
            // 初始化重试计数
            if (!(key in retry)) {
                retry[key] = 0;
            }
            
            const index = retry[key];
            
            // 检查重试次数
            if (index >= domains.length || index >= MAX_RETRIES) {
                console.error(`资源加载失败,已达最大重试次数: ${originalSrc}`);
                return;
            }
            
            const domain = domains[index];
            console.log(`重试加载: ${domain} (${index + 1}/${domains.length})`);
            
            // 同步加载(保证执行顺序)
            document.write(`<script src="${domain}"><\/script>`);
            
            retry[key]++;
        }, true);
    </script>
</head>
<body>
    <!-- 故意使用错误路径测试重试机制 -->
    <script src="wrong/path/app.js"></script>
    
    <div id="app">
        <h1>资源加载重试示例</h1>
    </div>
</body>
</html>

总结

本文介绍了一种基于 window.error 事件的 JavaScript 资源加载失败重试方案,包括:

  1. 两种实现方式:异步加载(createElement)和同步加载(document.write)
  2. 核心技术点:事件捕获、错误类型判断、重试计数器设计
  3. 实际应用场景:CDN 容灾、路径修正、版本回退
  4. 最佳实践:限制重试次数、监控上报、跨域处理

这个方案简单高效,可以显著提升前端应用的稳定性和用户体验。在实际项目中,建议结合具体业务场景选择合适的实现方式。

参考资料


如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐️、评论💬!

有任何问题欢迎在评论区讨论~

关于作者

专注前端性能优化和工程化实践,欢迎关注我的掘金账号获取更多优质内容!