跳到主要内容

资源加载失败处理

场景

线上页面的 JS/CSS/图片等静态资源偶尔加载失败(CDN 节点故障、网络波动),如何检测并自动恢复?

方案设计

1. 全局资源错误监听

resource-error-handler.ts
// 资源加载错误不会冒泡,必须在捕获阶段监听
window.addEventListener(
'error',
(event: ErrorEvent) => {
const target = event.target as HTMLElement;
if (!target || !('src' in target || 'href' in target)) return;

const tagName = target.tagName.toLowerCase();
const url = (target as HTMLScriptElement).src || (target as HTMLLinkElement).href;

console.error(`Resource load failed: [${tagName}] ${url}`);

// 分类型处理
switch (tagName) {
case 'script':
retryScript(target as HTMLScriptElement, url);
break;
case 'link':
retryStylesheet(target as HTMLLinkElement, url);
break;
case 'img':
handleImageError(target as HTMLImageElement);
break;
}
},
true // 捕获阶段
);

2. Script 重试(切换 CDN)

retry-script.ts
const CDN_LIST = [
'https://cdn1.example.com',
'https://cdn2.example.com',
'https://cdn3.example.com',
];

function retryScript(el: HTMLScriptElement, failedUrl: string, retryIndex = 0) {
if (retryIndex >= CDN_LIST.length) {
reportError({ type: 'script_load_fail', url: failedUrl });
return;
}

const path = new URL(failedUrl).pathname;
const newScript = document.createElement('script');
newScript.src = CDN_LIST[retryIndex] + path;
newScript.onerror = () => retryScript(el, failedUrl, retryIndex + 1);
document.head.appendChild(newScript);
}

3. 图片加载兜底

image-fallback.ts
function handleImageError(img: HTMLImageElement) {
const retryCount = Number(img.dataset.retryCount ?? 0);

if (retryCount < 2) {
// 第一次:加随机参数重试
img.dataset.retryCount = String(retryCount + 1);
img.src = img.src + (img.src.includes('?') ? '&' : '?') + `_t=${Date.now()}`;
} else {
// 兜底占位图
img.src = '/img/placeholder.png';
img.alt = '图片加载失败';
}
}

4. Service Worker 缓存兜底

sw-cache-fallback.ts
// Service Worker 中
self.addEventListener('fetch', (event: FetchEvent) => {
event.respondWith(
fetch(event.request)
.catch(() => {
// 网络失败时尝试读缓存
return caches.match(event.request);
})
.then((response) => {
return response ?? new Response('Resource unavailable', { status: 503 });
})
);
});

常见面试问题

Q1: 为什么 window.addEventListener('error') 必须设置 capture: true

答案

资源加载错误(script/link/img 的 onerror不会冒泡,只有在捕获阶段才能在 window 上监听到。如果不传第三个参数 true,就无法捕获资源加载失败事件。

Q2: Script 加载失败会导致什么后果?如何兜底?

答案

  1. 入口 JS 失败:页面白屏。兜底方案:多 CDN 自动切换 + <noscript> 提示
  2. 异步 chunk 失败:动态导入的模块无法加载。兜底方案:import().catch() 中重试,React.lazy 配合 ErrorBoundary
  3. 第三方 SDK 失败:依赖的功能不可用。兜底方案:try-catch 包裹调用 + 功能降级
// React 异步组件加载失败自动重试
const LazyComponent = React.lazy(() =>
import('./HeavyComponent').catch(() => {
// 重试一次
return import('./HeavyComponent');
})
);

相关链接