前端错误边界与降级策略

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 BoundaryApp 根组件包裹全局 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。


相关链接: