概述
本文档介绍如何在网页中实现脚本加载失败自动切换到备用CDN的重试机制。通过监听全局error
事件,当检测到脚本加载失败时,系统会自动尝试从备用CDN源重新加载脚本,直到成功或所有备用源耗尽。此实现方案能有效提高网站关键脚本的可用性,确保即使主CDN出现故障,用户仍然能正常使用网站功能。
实现原理
- 监听错误事件 :使用
window.addEventListener('error')
捕获页面错误 - 识别脚本错误:筛选出脚本加载失败的错误类型
- 重试机制:失败后切换到下一个备用CDN源
- 状态管理:跟踪当前尝试的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);
使用建议
-
CDN选择策略:
- 将最可靠的CDN放在列表首位
- 至少配置2-3个备用CDN源
- 考虑使用不同提供商的CDN以增加冗余
-
性能考虑:
- 重试机制会增加额外加载时间
- 设置合理的超时时间(本例未展示,实际可添加)
- 避免过多重试次数(一般3-4次为宜)
-
错误处理:
- 当所有CDN都失败时,提供友好的用户提示
- 考虑降级方案或本地备用资源
- 记录错误信息以便后期分析
-
安全考虑:
- 确保所有CDN源都是可信任的
- 使用HTTPS协议加载资源
- 考虑添加完整性校验(SRI)