前端错误边界与降级策略
What — 是什么
前端错误边界(Error Boundary)和降级策略是保证应用在异常情况下仍能提供可用体验的关键机制。错误边界隔离故障范围,防止单个组件崩溃导致整个应用白屏;降级策略在功能不可用时提供备选方案,确保核心流程始终可用。
核心概念:
- 错误边界(Error Boundary):React 中捕获子组件渲染错误、生命周期错误,显示降级 UI 而非白屏
- 优雅降级(Graceful Degradation):功能优先完整开发,然后为不支持的环境提供降级方案
- 渐进增强(Progressive Enhancement):先保证基础功能可用,再为高级环境增加特性
- 熔断(Circuit Breaker):连续失败时自动停止请求,防止级联故障
- 降级等级(Degradation Levels):P0 全功能 → P1 核心功能 → P2 基础功能 → P3 静态兜底
故障影响与防线:
┌───────────────────────────────────────────────────────────┐
│ 故障防线体系 │
├───────────────────────────────────────────────────────────┤
│ 故障来源 │
│ ├── JS 运行时错误(undefined.xxx、JSON.parse 异常) │
│ ├── 组件渲染错误(React render crash) │
│ ├── 网络请求失败(接口超时、5xx、CORS) │
│ ├── 资源加载失败(JS/CSS/图片 CDN 故障) │
│ ├── 浏览器兼容性(API 不支持、CSS 不渲染) │
│ └── 第三方服务故障(支付、地图、统计分析) │
├───────────────────────────────────────────────────────────┤
│ 防线层级 │
│ L1: Error Boundary → 组件级隔离,显示降级 UI │
│ L2: try/catch → 逻辑级捕获,静默处理 + 上报 │
│ L3: 全局错误监听 → window.onerror / unhandledrejection │
│ L4: 资源降级 → CDN 容灾、本地缓存兜底 │
│ L5: 功能降级 → 核心流程可用,非核心功能关闭 │
│ L6: 静态兜底 → SPA 完全不可用时显示静态 HTML │
└───────────────────────────────────────────────────────────┘
Why — 为什么
不处理错误的后果:
| 场景 | 不处理 | 处理后 |
|---|---|---|
| 组件渲染报错 | 整个应用白屏 | 只该组件显示”加载失败”,其余正常 |
| 接口超时 | 页面一直 Loading | 显示重试按钮 + 缓存数据 |
| CDN 故障 | 样式/JS 丢失,页面崩溃 | 自动切换备用 CDN |
| 第三方 SDK 报错 | 阻塞主流程 | 非核心功能降级,核心流程可用 |
| 大促流量高峰 | 服务端过载,全站 500 | 降级非核心功能,保核心交易 |
核心原则:
- 隔离:一个模块出错不影响其他模块
- 降级:功能不可用时提供备选方案
- 恢复:提供重试机制,让用户自助恢复
- 上报:错误信息上传监控系统,及时发现问题
- 优先级:核心流程 > 重要功能 > 锦上添花
How — 怎么用
React Error Boundary
// 基础 Error Boundary 组件
import { Component, ReactNode, ErrorInfo } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary caught:', error, errorInfo);
this.props.onError?.(error, errorInfo);
}
handleReset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div style={{ padding: 24, textAlign: 'center' }}>
<h3>出错了</h3>
<p>该模块暂时无法显示</p>
<button onClick={this.handleReset}>重试</button>
</div>
);
}
return this.props.children;
}
}
// 使用:包裹可能出错的组件
function App() {
return (
<ErrorBoundary fallback={<div>地图模块暂不可用</div>}>
<MapComponent />
</ErrorBoundary>
);
}
函数式 Error Boundary(使用 react-error-boundary):
npm install react-error-boundary
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert" style={{ padding: 24, textAlign: 'center', background: '#fff2f0', borderRadius: 8 }}>
<h3 style={{ color: '#ff4d4f' }}>加载失败</h3>
<p style={{ color: '#666', fontSize: 14 }}>{error.message}</p>
<button
onClick={resetErrorBoundary}
style={{ marginTop: 12, padding: '6px 16px', background: '#1677ff', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer' }}
>
重试
</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// 重置状态(如清除缓存、重置 query)
queryClient.resetQueries();
}}
onError={(error, info) => {
// 上报错误
reportError(error, info);
}}
>
<Dashboard />
</ErrorBoundary>
);
}
分层 Error Boundary
// 不同层级使用不同的降级策略
function App() {
return (
// L1: 全局兜底 — 最严重的错误才到这里
<ErrorBoundary FallbackComponent={GlobalFallback}>
<Header />
<main>
{/* L2: 区域级 — 侧边栏崩溃不影响主内容 */}
<ErrorBoundary FallbackComponent={SidebarFallback}>
<Sidebar />
</ErrorBoundary>
{/* L2: 区域级 — 主内容崩溃不影响侧边栏 */}
<ErrorBoundary FallbackComponent={ContentFallback}>
{/* L3: 组件级 — 单个卡片崩溃不影响其他卡片 */}
<div className="card-grid">
{widgets.map((widget) => (
<ErrorBoundary
key={widget.id}
fallback={
<div className="card card--error">
<p>{widget.name}暂不可用</p>
</div>
}
>
<WidgetRenderer widget={widget} />
</ErrorBoundary>
))}
</div>
</ErrorBoundary>
</main>
<Footer />
</ErrorBoundary>
);
}
// 不同层级的降级 UI
function GlobalFallback({ resetErrorBoundary }: FallbackProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100vh' }}>
<h1>页面出了点问题</h1>
<p>我们正在修复,请稍后重试</p>
<button onClick={resetErrorBoundary}>刷新页面</button>
<a href="/" style={{ marginTop: 12 }}>返回首页</a>
</div>
);
}
function ContentFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div style={{ padding: 48, textAlign: 'center' }}>
<h2>内容加载失败</h2>
<button onClick={resetErrorBoundary}>重新加载</button>
</div>
);
}
全局错误监听
// utils/globalErrorHandler.ts
// 1. JS 运行时错误
window.onerror = (message, source, lineno, colno, error) => {
console.error('Global error:', { message, source, lineno, colno, error });
reportError({
type: 'runtime',
message: String(message),
stack: error?.stack,
source,
lineno,
colno,
});
return false; // 不阻止默认行为
};
// 2. Promise 未捕获的 rejection
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled rejection:', event.reason);
reportError({
type: 'promise',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
});
// 非核心错误:静默处理,不白屏
event.preventDefault();
});
// 3. 资源加载错误(img/script/link)
window.addEventListener('error', (event) => {
const target = event.target as HTMLElement;
if (target.tagName) {
const url = (target as HTMLImageElement).src || (target as HTMLScriptElement).src || (target as HTMLLinkElement).href;
console.warn('Resource failed:', target.tagName, url);
reportError({ type: 'resource', tagName: target.tagName, url });
}
}, true); // 捕获阶段
// 4. React 18 错误覆盖(开发模式)
// 生产环境不使用 console.error 覆盖
网络请求降级
// utils/resilientRequest.ts
import { reportError } from './monitor';
interface RequestConfig {
url: string;
method?: string;
data?: any;
retries?: number;
retryDelay?: number;
timeout?: number;
fallback?: any; // 降级数据
useCache?: boolean; // 是否使用缓存
cacheKey?: string;
cacheTTL?: number; // 缓存有效期(ms)
}
// 简易内存缓存
const cache = new Map<string, { data: any; expireAt: number }>();
function getCache(key: string) {
const entry = cache.get(key);
if (!entry) return null;
if (Date.now() > entry.expireAt) { cache.delete(key); return null; }
return entry.data;
}
function setCache(key: string, data: any, ttl: number) {
cache.set(key, { data, expireAt: Date.now() + ttl });
}
async function resilientRequest<T = any>(config: RequestConfig): Promise<T> {
const {
url, method = 'GET', data, retries = 2, retryDelay = 1000,
timeout = 10000, fallback, useCache = true, cacheKey = url, cacheTTL = 5 * 60 * 1000,
} = config;
// 1. 优先使用缓存
if (useCache) {
const cached = getCache(cacheKey);
if (cached) return cached;
}
let lastError: Error | null = null;
// 2. 重试循环
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: data ? JSON.stringify(data) : undefined,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
// 成功:更新缓存
if (useCache) setCache(cacheKey, result, cacheTTL);
return result;
} catch (error: any) {
lastError = error;
// 超时/网络错误:重试
if (attempt < retries && (error.name === 'AbortError' || error.message.includes('Failed to fetch'))) {
await new Promise((r) => setTimeout(r, retryDelay * (attempt + 1))); // 指数退避
continue;
}
// 不重试的错误(如 4xx)
break;
}
}
// 3. 请求失败:使用缓存
if (useCache) {
const cached = getCache(cacheKey);
if (cached) {
console.warn(`Request failed, using stale cache: ${url}`);
return cached;
}
}
// 4. 缓存也没有:使用降级数据
if (fallback !== undefined) {
console.warn(`Request failed, using fallback data: ${url}`);
return fallback;
}
// 5. 完全无法降级:上报并抛出
reportError({ type: 'api', url, error: lastError?.message });
throw lastError;
}
熔断器模式
// utils/circuitBreaker.ts
enum CircuitState { Closed, Open, HalfOpen }
class CircuitBreaker {
private state = CircuitState.Closed;
private failureCount = 0;
private lastFailureTime = 0;
private successCount = 0;
constructor(
private threshold = 5, // 连续失败次数阈值
private resetTimeout = 30000, // 熔断恢复时间(ms)
private halfOpenMax = 2, // 半开状态最大试探次数
) {}
async execute<T>(fn: () => Promise<T>, fallback?: () => T | Promise<T>): Promise<T> {
if (this.state === CircuitState.Open) {
if (Date.now() - this.lastFailureTime > this.resetTimeout) {
this.state = CircuitState.HalfOpen;
this.successCount = 0;
} else {
// 熔断中:直接走降级
if (fallback) return fallback();
throw new Error('Circuit is open');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
if (fallback) return fallback();
throw error;
}
}
private onSuccess() {
this.failureCount = 0;
if (this.state === CircuitState.HalfOpen) {
this.successCount++;
if (this.successCount >= this.halfOpenMax) {
this.state = CircuitState.Closed;
}
}
}
private onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.state === CircuitState.HalfOpen || this.failureCount >= this.threshold) {
this.state = CircuitState.Open;
}
}
getState() { return this.state; }
}
// 使用
const paymentCircuit = new CircuitBreaker(3, 30000);
async function processPayment(order: Order) {
return paymentCircuit.execute(
() => api.post('/pay/create', order),
() => {
// 降级:提示稍后重试
showToast('支付服务繁忙,请稍后重试');
throw new Error('Payment service unavailable');
},
);
}
功能降级管理
// utils/degradation.ts
type DegradationLevel = 'full' | 'core' | 'minimal';
// 功能开关配置
const featureConfig: Record<string, { level: DegradationLevel; fallback?: () => void }> = {
// P0: 核心功能,尽可能不降级
'product-list': { level: 'core' },
'checkout': { level: 'core' },
'user-auth': { level: 'core' },
// P1: 重要功能,可降级
'product-recommend': { level: 'full', fallback: () => showStaticRecommend() },
'search-suggest': { level: 'full', fallback: () => disableAutoSuggest() },
'live-chat': { level: 'full', fallback: () => showOfflineMessage() },
// P2: 锦上添花,优先降级
'animated-background': { level: 'full', fallback: () => useStaticBackground() },
'3d-preview': { level: 'full', fallback: () => show2DImages() },
'social-share': { level: 'full', fallback: () => hideShareButtons() },
'comments': { level: 'full', fallback: () => hideComments() },
};
let currentLevel: DegradationLevel = 'full';
export function setDegradationLevel(level: DegradationLevel) {
currentLevel = level;
// 通知所有组件重新渲染
window.dispatchEvent(new CustomEvent('degradation-change', { detail: { level } }));
}
export function isFeatureAvailable(feature: string): boolean {
const config = featureConfig[feature];
if (!config) return true;
const levelPriority: Record<DegradationLevel, number> = { full: 3, core: 2, minimal: 1 };
return levelPriority[config.level] <= levelPriority[currentLevel];
}
export function useFeature(feature: string) {
const [available, setAvailable] = useState(isFeatureAvailable(feature));
useEffect(() => {
const handler = () => setAvailable(isFeatureAvailable(feature));
window.addEventListener('degradation-change', handler);
return () => window.removeEventListener('degradation-change', handler);
}, [feature]);
// 不可用时自动执行降级
useEffect(() => {
if (!available) {
featureConfig[feature]?.fallback?.();
}
}, [available, feature]);
return available;
}
// React 组件中使用
function ProductPage() {
const show3D = useFeature('3d-preview');
const showRecommend = useFeature('product-recommend');
return (
<div>
{/* 核心功能始终可用 */}
<ProductDetail product={product} />
{/* 降级组件 */}
{show3D ? <Model3DViewer url={product.modelUrl} /> : <ImageGallery images={product.images} />}
{showRecommend ? <SmartRecommend /> : <StaticRecommend />}
</div>
);
}
// 自动降级:根据错误率动态调整
class DegradationManager {
private errorRates: Map<string, number[]> = new Map();
reportError(feature: string) {
const rates = this.errorRates.get(feature) || [];
rates.push(Date.now());
// 只保留最近 5 分钟的错误
const cutoff = Date.now() - 5 * 60 * 1000;
this.errorRates.set(feature, rates.filter(t => t > cutoff));
// 检查是否需要降级
this.checkDegradation();
}
private checkDegradation() {
let totalErrors = 0;
this.errorRates.forEach((rates) => { totalErrors += rates.length; });
if (totalErrors > 50) setDegradationLevel('minimal');
else if (totalErrors > 20) setDegradationLevel('core');
else setDegradationLevel('full');
}
}
CDN 容灾
// utils/cdnFallback.ts
const CDN_LIST = [
'https://cdn1.example.com',
'https://cdn2.example.com',
'https://cdn3.example.com',
];
// 脚本加载降级
function loadScriptWithFallback(paths: string[]): Promise<void> {
return new Promise((resolve, reject) => {
let index = 0;
function tryLoad() {
if (index >= paths.length) {
reject(new Error('All CDN sources failed'));
return;
}
const script = document.createElement('script');
script.src = paths[index];
script.onload = () => resolve();
script.onerror = () => {
index++;
console.warn(`CDN failed: ${paths[index - 1]}, trying next...`);
tryLoad();
};
document.head.appendChild(script);
}
tryLoad();
});
}
// 图片降级
function loadImageWithFallback(primaryUrl: string, fallbackUrl: string): Promise<string> {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(primaryUrl);
img.onerror = () => {
console.warn(`Image CDN failed: ${primaryUrl}`);
resolve(fallbackUrl);
};
img.src = primaryUrl;
});
}
// React 组件中使用
function SafeImage({ src, fallbackSrc, alt }: { src: string; fallbackSrc: string; alt: string }) {
const [imgSrc, setImgSrc] = useState(src);
return (
<img
src={imgSrc}
alt={alt}
onError={() => {
if (imgSrc !== fallbackSrc) setImgSrc(fallbackSrc);
}}
/>
);
}
第三方 SDK 容错
// utils/safeSDK.ts
// 安全调用第三方 SDK
function safeCall<T>(fn: () => T, fallback: T, label: string): T {
try {
return fn();
} catch (error) {
console.warn(`SDK call failed [${label}]:`, error);
reportError({ type: 'sdk', label, error: String(error) });
return fallback;
}
}
// 使用示例
// 1. 微信 SDK
function safeWxShare(shareData: ShareData) {
safeCall(() => {
wx.updateAppMessageShareData(shareData);
}, undefined, 'wx.share');
}
// 2. 数据分析 SDK
function safeTrack(event: string, data?: Record<string, any>) {
safeCall(() => {
analytics.track(event, data);
}, undefined, 'analytics.track');
}
// 3. 地图 SDK
function safeMapInit(container: string) {
return safeCall(() => {
return new AMap.Map(container, { zoom: 12 });
}, null, 'amap.init');
}
// 4. 支付 SDK — 异步版本
async function safeAsyncCall<T>(
fn: () => Promise<T>,
fallback: T,
label: string
): Promise<T> {
try {
return await Promise.race([
fn(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('SDK timeout')), 10000)
),
]);
} catch (error) {
console.warn(`Async SDK call failed [${label}]:`, error);
reportError({ type: 'sdk-async', label, error: String(error) });
return fallback;
}
}
静态兜底页面
<!-- public/fallback.html — SPA 完全不可用时显示 -->
<!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: -apple-system, BlinkMacSystemFont, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
.container { text-align: center; padding: 48px; background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); max-width: 480px; }
h1 { font-size: 1.5rem; color: #333; margin-bottom: 12px; }
p { color: #666; line-height: 1.6; margin-bottom: 24px; }
.btn { display: inline-block; padding: 10px 24px; background: #1677ff; color: #fff; border: none; border-radius: 6px; cursor: pointer; text-decoration: none; }
</style>
</head>
<body>
<div class="container">
<h1>页面暂时不可用</h1>
<p>我们正在紧急修复中,请稍后再试。如有紧急需求,请联系客服。</p>
<a class="btn" href="/">返回首页</a>
<a class="btn" href="javascript:location.reload()" style="background:#fff;color:#1677ff;border:1px solid #1677ff;margin-left:8px;">刷新重试</a>
</div>
<script>
// Service Worker 兜底:拦截请求返回此页面
// 5 秒后自动重试
setTimeout(() => location.reload(), 5000);
</script>
</body>
</html>
Service Worker 兜底:
// public/sw.js
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.catch(() => {
// 主站不可用时返回兜底页面
if (event.request.mode === 'navigate') {
return caches.match('/fallback.html');
}
return new Response('Service Unavailable', { status: 503 });
})
);
});
错误上报
// utils/errorReporter.ts
interface ErrorReport {
type: 'runtime' | 'promise' | 'resource' | 'api' | 'sdk' | 'render';
message: string;
stack?: string;
url?: string;
lineno?: number;
colno?: number;
tagName?: string;
timestamp?: number;
userAgent?: string;
extra?: Record<string, any>;
}
const errorQueue: ErrorReport[] = [];
let flushTimer: ReturnType<typeof setTimeout> | null = null;
function addToQueue(report: ErrorReport) {
errorQueue.push({
...report,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: location.href,
});
// 批量上报:收集 5 条或 3 秒后上报
if (errorQueue.length >= 5) {
flush();
} else if (!flushTimer) {
flushTimer = setTimeout(flush, 3000);
}
}
async function flush() {
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
if (errorQueue.length === 0) return;
const reports = errorQueue.splice(0);
try {
// 使用 sendBeacon 保证页面卸载时也能上报
const success = navigator.sendBeacon?.('/api/error-report', JSON.stringify(reports));
if (!success) {
await fetch('/api/error-report', { method: 'POST', body: JSON.stringify(reports), keepalive: true });
}
} catch {
// 上报本身失败则静默丢弃
}
}
export function reportError(report: ErrorReport) {
// 开发环境直接打印
if (process.env.NODE_ENV === 'development') {
console.error('[ErrorReport]', report);
}
addToQueue(report);
}
// 页面卸载时强制上报
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') flush();
});
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Error Boundary 不捕获事件处理错误 | 只捕获渲染/生命周期错误 | 事件处理器中用 try/catch |
| async 函数错误穿透 | Error Boundary 不捕获异步错误 | useAsync + try/catch |
| 白屏无任何降级 | 最外层没有 Error Boundary | App 根组件包裹全局 ErrorBoundary |
| 降级 UI 不合适 | fallback 太简陋 | 按业务场景设计不同的降级 UI |
| CDN 全挂 | 单一 CDN 无容灾 | 多 CDN 降级 + 本地缓存 |
| 第三方 SDK 卡死 | SDK 加载超时 | 异步加载 + 超时熔断 |
| 缓存数据过期 | 缓存 TTL 太长 | 合理设置 TTL + 版本号 |
| 熔断后无法恢复 | resetTimeout 太短/太长 | 根据业务调整,一般 30s |
| 降级等级未分级 | 所有功能同等对待 | 按业务优先级分 P0/P1/P2 |
最佳实践
- 最外层必须有全局 Error Boundary,防止白屏
- 组件级按业务粒度加 Error Boundary,隔离故障范围
- 事件处理和异步函数中用 try/catch,Error Boundary 捕获不到
- 网络请求三级降级:缓存 → 降级数据 → 错误提示 + 重试
- 第三方 SDK 全部用安全封装(safeCall + 超时 + 降级)
- 功能按业务优先级分级(P0/P1/P2),大促时自动降级非核心功能
- CDN 多源容灾 + 图片/脚本加载失败降级
- 错误上报批量合并 + sendBeacon 保证可靠性
- 静态兜底页面作为最后防线,Service Worker 缓存
- 定期演练降级场景,确保降级链路可用
面试题
Q1: React Error Boundary 能捕获哪些错误?不能捕获哪些?
能捕获:① 渲染期间的错误(render 方法、函数组件返回值);② 生命周期方法中的错误(componentDidMount 等);③ 子组件构造函数中的错误。不能捕获:① 事件处理器中的错误(需自行 try/catch);② 异步代码中的错误(setTimeout/Promise/requestAnimationFrame);③ 服务端渲染的错误;④ Error Boundary 自身的错误。React 这样设计是因为事件处理器和异步代码不发生在渲染流程中,Error Boundary 只在渲染阶段生效。
Q2: 如何实现 React 异步组件的错误边界?
两种方式:① Suspense + ErrorBoundary 配合:
<ErrorBoundary fallback={<Error/>}><Suspense fallback={<Loading/>}><AsyncComponent/></Suspense></ErrorBoundary>,React 18 的 Suspense 可捕获数据获取错误;② 自定义 hook 封装:useAsync在 hook 内 try/catch 捕获异步错误,将错误转为状态,组件根据状态渲染降级 UI。关键点:异步错误不会冒泡到 ErrorBoundary,必须在异步函数内部处理或通过状态传递到渲染阶段触发 ErrorBoundary。
Q3: 熔断器模式的工作原理是什么?
三个状态:① Closed(关闭):正常请求,计数失败次数,超过阈值切换到 Open;② Open(打开):直接拒绝请求(走降级),等待 resetTimeout 后切换到 HalfOpen;③ HalfOpen(半开):放行少量试探请求,成功则切换回 Closed,失败则切换回 Open。核心思想:当下游服务持续故障时,快速失败比等待超时更好,减少无效等待和资源浪费。恢复机制:定期试探,成功则恢复,避免永久熔断。
Q4: 前端如何实现 CDN 容灾?
多级容灾方案:① 多 CDN 源降级:配置多个 CDN 域名,加载失败时自动切换下一个;② 本地缓存兜底:Service Worker 缓存关键资源,CDN 全挂时从缓存加载;③ 静态兜底页面:SW 拦截导航请求,主站不可用时返回预缓存的 fallback.html。脚本降级代码:按顺序尝试 CDN1 → CDN2 → CDN3,全部失败时显示兜底提示。图片降级:
<img onerror="this.src=fallbackUrl">切换到备用图。
Q5: 如何设计功能降级等级?大促场景下怎么自动降级?
降级等级:P0 全功能(正常)、P1 核心功能(关闭推荐/评论/3D等)、P2 基础功能(只保留浏览+下单+登录)、P3 静态兜底(纯 HTML)。自动降级机制:① 错误率监控:5 分钟内错误数超阈值自动降一级;② 接口耗时监控:P99 > 阈值时降级非核心接口;③ 人工开关:运维通过配置中心手动降级;④ 流量预估:大促前预降级,活动后恢复。实现:Feature Flag + DegradationManager,组件通过
useFeature('3d-preview')判断是否可用。
Q6: 前端错误上报如何保证可靠性?
三个关键点:① sendBeacon:页面卸载时仍能发送请求,数据放入浏览器队列,不受 unload 影响;② 批量合并:收集 5 条或 3 秒后批量上报,减少请求数,失败时缓存重试;③ 降级兜底:sendBeacon 不可用时用 fetch + keepalive,再不行用 Image 1x1 像素打点。另外:重复错误去重(相同 message+stack 5 分钟内只报一次)、采样率控制(高频错误 1% 采样)、关键错误实时告警。
Q7: 第三方 SDK 集成时如何做容错?
四层防护:① 异步加载:SDK 不阻塞主流程,
<script async>或动态插入;② 超时控制:Promise.race 设置 10s 超时,超时走降级;③ safeCall 封装:所有 SDK 调用包裹 try/catch,失败静默 + 上报;④ 功能降级:SDK 不可用时关闭对应功能(如地图不可用 → 显示文字地址)。原则:第三方 SDK 永远不可信,假设它会崩溃/卡死/报错,核心流程不依赖第三方。
Q8: Service Worker 在错误降级中能做什么?
三个能力:① 离线缓存:预缓存关键资源(HTML/JS/CSS),CDN 故障时从缓存加载;② 导航兜底:拦截导航请求,主站不可用时返回预缓存的 fallback.html;③ 请求拦截:API 请求失败时返回缓存的响应(stale-while-revalidate 策略)。实现:SW install 时预缓存 fallback.html 和关键页面;fetch 事件中 try network → catch cache → catch fallback;页面卸载时强制上报错误(SW 可监听 push 事件做后台同步)。限制:首次访问无缓存,SW 注册需 HTTPS。
相关链接:
- 前端监控与错误追踪
- 前端性能监控体系
- 前端架构模式
- Service Worker与PWA
- 前端安全体系
- CI/CD与部署