前言
在实际生产环境中,我们经常会遇到 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 资源加载失败重试方案,包括:
- 两种实现方式:异步加载(createElement)和同步加载(document.write)
- 核心技术点:事件捕获、错误类型判断、重试计数器设计
- 实际应用场景:CDN 容灾、路径修正、版本回退
- 最佳实践:限制重试次数、监控上报、跨域处理
这个方案简单高效,可以显著提升前端应用的稳定性和用户体验。在实际项目中,建议结合具体业务场景选择合适的实现方式。
参考资料
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐️、评论💬!
有任何问题欢迎在评论区讨论~
关于作者
专注前端性能优化和工程化实践,欢迎关注我的掘金账号获取更多优质内容!