Skip to content

错误边界最佳实践

第一部分:设计原则

1.1 粒度控制原则

javascript
// 原则1:关键功能独立边界
function App() {
  return (
    <div>
      {/* 静态内容:无需边界 */}
      <Header />
      
      {/* 关键功能:独立边界 */}
      <ErrorBoundary name="payment" fallback={<PaymentError />}>
        <PaymentModule />
      </ErrorBoundary>
      
      {/* 用户生成内容:独立边界 */}
      <ErrorBoundary name="content" fallback={<ContentError />}>
        <UserContent />
      </ErrorBoundary>
      
      {/* 第三方组件:独立边界 */}
      <ErrorBoundary name="chat" fallback={<ChatError />}>
        <ThirdPartyChat />
      </ErrorBoundary>
      
      {/* 静态内容:无需边界 */}
      <Footer />
    </div>
  );
}

// 原则2:避免过度嵌套
// ❌ 过度嵌套
function OverNested() {
  return (
    <ErrorBoundary>
      <ErrorBoundary>
        <ErrorBoundary>
          <Content />
        </ErrorBoundary>
      </ErrorBoundary>
    </ErrorBoundary>
  );
}

// ✅ 合理嵌套
function ReasonableNesting() {
  return (
    <ErrorBoundary name="app" fallback={<AppError />}>
      <Page />
    </ErrorBoundary>
  );
}

function Page() {
  return (
    <div>
      <Header />
      
      <ErrorBoundary name="content" fallback={<ContentError />}>
        <MainContent />
      </ErrorBoundary>
    </div>
  );
}

1.2 用户体验原则

javascript
// 原则3:提供有意义的fallback
// ❌ 无用的错误提示
<ErrorBoundary fallback={<div>Error</div>}>
  <Component />
</ErrorBoundary>

// ✅ 有意义的fallback
<ErrorBoundary 
  fallback={
    <div className="error-fallback">
      <Icon name="warning" />
      <h2>功能暂时不可用</h2>
      <p>我们正在修复这个问题,请稍后再试</p>
      <button onClick={() => window.location.reload()}>
        刷新页面
      </button>
    </div>
  }
>
  <Component />
</ErrorBoundary>

// 原则4:保留尽可能多的功能
function PreserveFunction() {
  return (
    <div className="dashboard">
      {/* 导航始终可用 */}
      <Navigation />
      
      {/* 主功能独立保护 */}
      <ErrorBoundary fallback={<WidgetError />}>
        <StatisticsWidget />
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<ChartError />}>
        <ChartWidget />
      </ErrorBoundary>
      
      {/* 次要功能独立保护 */}
      <ErrorBoundary fallback={null}>
        <RecommendationWidget />
      </ErrorBoundary>
    </div>
  );
}

// 原则5:提供恢复机制
class RecoverableErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div className="recoverable-error">
          <h3>出错了</h3>
          <div className="actions">
            <button onClick={() => this.setState({ hasError: false })}>
              重试
            </button>
            <button onClick={() => window.location.reload()}>
              刷新页面
            </button>
            <button onClick={() => window.history.back()}>
              返回上一页
            </button>
          </div>
        </div>
      );
    }
    
    return this.props.children;
  }
}

1.3 错误监控原则

javascript
// 原则6:完整的错误上下文
class ContextualErrorBoundary extends React.Component {
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    const context = {
      // 错误详情
      error: {
        message: error.message,
        stack: error.stack,
        name: error.name
      },
      
      // 组件信息
      component: {
        name: this.constructor.name,
        props: this.props,
        state: this.state,
        stack: errorInfo.componentStack
      },
      
      // 环境信息
      environment: {
        url: window.location.href,
        userAgent: navigator.userAgent,
        timestamp: new Date().toISOString(),
        nodeEnv: process.env.NODE_ENV
      },
      
      // 用户信息
      user: {
        id: this.props.userId,
        session: sessionStorage.getItem('sessionId')
      }
    };
    
    this.reportError(context);
  }
  
  reportError(context) {
    // 发送到监控服务
  }
}

// 原则7:区分错误严重程度
function ErrorSeverity() {
  return (
    <div>
      {/* 致命错误:阻止使用 */}
      <ErrorBoundary 
        severity="critical"
        fallback={<CriticalErrorPage />}
      >
        <PaymentProcessor />
      </ErrorBoundary>
      
      {/* 重要错误:降级功能 */}
      <ErrorBoundary 
        severity="high"
        fallback={<DegradedFeature />}
      >
        <MainFeature />
      </ErrorBoundary>
      
      {/* 一般错误:静默处理 */}
      <ErrorBoundary 
        severity="low"
        fallback={null}
        onError={logError}
      >
        <NonCriticalWidget />
      </ErrorBoundary>
    </div>
  );
}

第二部分:实现模式

2.1 通用错误边界

javascript
// 可复用的通用错误边界
class UniversalErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      errorInfo: null,
      errorId: null
    };
  }
  
  static getDerivedStateFromError(error) {
    return {
      hasError: true,
      error,
      errorId: generateErrorId()
    };
  }
  
  componentDidCatch(error, errorInfo) {
    const { onError, reportToService } = this.props;
    
    // 更新state
    this.setState({ errorInfo });
    
    // 自定义错误处理
    if (onError) {
      onError(error, errorInfo);
    }
    
    // 上报服务
    if (reportToService !== false) {
      this.reportError(error, errorInfo);
    }
  }
  
  reportError(error, errorInfo) {
    const { boundaryName, userId } = this.props;
    
    fetch('/api/errors', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        errorId: this.state.errorId,
        boundaryName,
        userId,
        error: {
          message: error.message,
          stack: error.stack
        },
        errorInfo: {
          componentStack: errorInfo.componentStack
        },
        timestamp: Date.now()
      })
    });
  }
  
  reset = () => {
    this.setState({
      hasError: false,
      error: null,
      errorInfo: null,
      errorId: null
    });
  };
  
  render() {
    const { hasError, error, errorInfo, errorId } = this.state;
    const { fallback, FallbackComponent, children } = this.props;
    
    if (hasError) {
      // 自定义FallbackComponent
      if (FallbackComponent) {
        return (
          <FallbackComponent
            error={error}
            errorInfo={errorInfo}
            errorId={errorId}
            reset={this.reset}
          />
        );
      }
      
      // 简单fallback
      if (fallback) {
        return fallback;
      }
      
      // 默认fallback
      return (
        <div className="default-error-fallback">
          <h2>出错了</h2>
          <p>错误ID: {errorId}</p>
          <button onClick={this.reset}>重试</button>
        </div>
      );
    }
    
    return children;
  }
}

function generateErrorId() {
  return `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}

// 使用示例
function App() {
  return (
    <UniversalErrorBoundary
      boundaryName="main-app"
      userId={currentUser.id}
      FallbackComponent={CustomErrorPage}
      onError={(error, errorInfo) => {
        console.error('App error:', error);
      }}
    >
      <MainApp />
    </UniversalErrorBoundary>
  );
}

2.2 路由级错误边界

javascript
// 路由专用错误边界
function AppRouter() {
  return (
    <BrowserRouter>
      <Routes>
        <Route
          path="/"
          element={
            <RouteErrorBoundary>
              <Home />
            </RouteErrorBoundary>
          }
        />
        
        <Route
          path="/dashboard"
          element={
            <RouteErrorBoundary>
              <Dashboard />
            </RouteErrorBoundary>
          }
        />
      </Routes>
    </BrowserRouter>
  );
}

class RouteErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    const currentRoute = window.location.pathname;
    
    logError({
      error,
      errorInfo,
      route: currentRoute,
      referrer: document.referrer
    });
  }
  
  componentDidUpdate(prevProps) {
    // 路由变化时重置错误
    if (this.props.location !== prevProps.location) {
      this.setState({ hasError: false });
    }
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div className="route-error">
          <h1>页面加载失败</h1>
          <button onClick={() => window.history.back()}>
            返回上一页
          </button>
          <button onClick={() => window.location.href = '/'}>
            返回首页
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// 高阶组件包装
function withErrorBoundary(Component, errorBoundaryProps) {
  return function WithErrorBoundaryComponent(props) {
    return (
      <ErrorBoundary {...errorBoundaryProps}>
        <Component {...props} />
      </ErrorBoundary>
    );
  };
}

// 使用
const SafeProfile = withErrorBoundary(UserProfile, {
  fallback: <ProfileError />,
  onError: logProfileError
});

2.3 异步边界模式

javascript
// 处理异步加载错误
import { lazy, Suspense } from 'react';

const LazyComponent = lazy(() => 
  import('./Component').catch(error => {
    // 捕获chunk加载错误
    console.error('Chunk load error:', error);
    return { default: () => <ChunkLoadError /> };
  })
);

function AsyncErrorBoundary({ children }) {
  return (
    <ErrorBoundary
      fallback={<AsyncError />}
      onError={(error) => {
        if (error.name === 'ChunkLoadError') {
          // 特殊处理chunk加载错误
          window.location.reload();
        }
      }}
    >
      <Suspense fallback={<Loading />}>
        {children}
      </Suspense>
    </ErrorBoundary>
  );
}

// 使用
function App() {
  return (
    <AsyncErrorBoundary>
      <LazyComponent />
    </AsyncErrorBoundary>
  );
}

第三部分:错误处理策略

3.1 分层错误处理

javascript
// 三层错误处理架构
function LayeredErrorHandling() {
  return (
    // 第一层:应用级
    <AppErrorBoundary>
      <App />
    </AppErrorBoundary>
  );
}

function App() {
  return (
    <div>
      <GlobalHeader />
      
      {/* 第二层:页面级 */}
      <PageErrorBoundary>
        <CurrentPage />
      </PageErrorBoundary>
      
      <GlobalFooter />
    </div>
  );
}

function CurrentPage() {
  return (
    <div>
      <PageHeader />
      
      {/* 第三层:组件级 */}
      <ComponentErrorBoundary>
        <ComplexWidget />
      </ComponentErrorBoundary>
      
      <ComponentErrorBoundary>
        <AnotherWidget />
      </ComponentErrorBoundary>
    </div>
  );
}

// 每层有不同的处理策略
class AppErrorBoundary extends React.Component {
  // 应用级:记录、报警、显示全局错误页
}

class PageErrorBoundary extends React.Component {
  // 页面级:记录、部分降级
}

class ComponentErrorBoundary extends React.Component {
  // 组件级:记录、隐藏组件或显示占位符
}

3.2 错误降级策略

javascript
// 渐进式降级
class GracefulDegradationBoundary extends React.Component {
  state = { 
    hasError: false, 
    degradationLevel: 0 
  };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    const newLevel = this.state.degradationLevel + 1;
    this.setState({ degradationLevel: newLevel });
    
    logError(error, errorInfo, { degradationLevel: newLevel });
  }
  
  render() {
    const { degradationLevel } = this.state;
    
    // 正常模式
    if (degradationLevel === 0) {
      return <FullFeatureComponent />;
    }
    
    // 第一级降级:简化功能
    if (degradationLevel === 1) {
      return <SimplifiedComponent />;
    }
    
    // 第二级降级:基础功能
    if (degradationLevel === 2) {
      return <BasicComponent />;
    }
    
    // 完全降级:静态内容
    return <StaticFallback />;
  }
}

3.3 错误恢复策略

javascript
// 智能恢复
class SmartRecoveryBoundary extends React.Component {
  state = {
    hasError: false,
    retryCount: 0,
    lastError: null
  };
  
  static getDerivedStateFromError(error) {
    return { hasError: true, lastError: error };
  }
  
  componentDidCatch(error, errorInfo) {
    this.logError(error, errorInfo);
    
    // 自动恢复逻辑
    if (this.canAutoRecover(error)) {
      this.scheduleRecovery();
    }
  }
  
  canAutoRecover(error) {
    // 可恢复的错误类型
    const recoverableTypes = [
      'NetworkError',
      'TimeoutError',
      'TemporaryUnavailable'
    ];
    
    return recoverableTypes.some(type => 
      error.name.includes(type)
    ) && this.state.retryCount < 3;
  }
  
  scheduleRecovery() {
    const delay = Math.min(1000 * Math.pow(2, this.state.retryCount), 10000);
    
    setTimeout(() => {
      this.setState(prev => ({
        hasError: false,
        retryCount: prev.retryCount + 1
      }));
    }, delay);
  }
  
  manualRetry = () => {
    this.setState({
      hasError: false,
      retryCount: 0
    });
  };
  
  render() {
    if (this.state.hasError) {
      const { retryCount, lastError } = this.state;
      
      return (
        <div className="error-recovery">
          <h2>出错了</h2>
          <p>{lastError?.message}</p>
          
          {retryCount < 3 ? (
            <div>
              <p>正在尝试恢复... (第 {retryCount + 1} 次)</p>
              <button onClick={this.manualRetry}>立即重试</button>
            </div>
          ) : (
            <div>
              <p>无法自动恢复</p>
              <button onClick={() => window.location.reload()}>
                刷新页面
              </button>
            </div>
          )}
        </div>
      );
    }
    
    return this.props.children;
  }
}

第四部分:测试与调试

4.1 错误边界测试

javascript
// 测试工具
import { render, screen } from '@testing-library/react';

describe('ErrorBoundary', () => {
  beforeEach(() => {
    // 抑制React错误日志
    jest.spyOn(console, 'error').mockImplementation(() => {});
  });
  
  afterEach(() => {
    console.error.mockRestore();
  });
  
  it('should render fallback on error', () => {
    const ThrowError = () => {
      throw new Error('Test error');
    };
    
    render(
      <ErrorBoundary fallback={<div>Error occurred</div>}>
        <ThrowError />
      </ErrorBoundary>
    );
    
    expect(screen.getByText('Error occurred')).toBeInTheDocument();
  });
  
  it('should call onError callback', () => {
    const onError = jest.fn();
    const ThrowError = () => {
      throw new Error('Test error');
    };
    
    render(
      <ErrorBoundary onError={onError}>
        <ThrowError />
      </ErrorBoundary>
    );
    
    expect(onError).toHaveBeenCalled();
    expect(onError.mock.calls[0][0].message).toBe('Test error');
  });
  
  it('should reset error on retry', () => {
    const ThrowError = ({ shouldThrow }) => {
      if (shouldThrow) {
        throw new Error('Test error');
      }
      return <div>Success</div>;
    };
    
    const { rerender } = render(
      <ErrorBoundary>
        <ThrowError shouldThrow={true} />
      </ErrorBoundary>
    );
    
    expect(screen.getByText(/error/i)).toBeInTheDocument();
    
    // 模拟重置
    rerender(
      <ErrorBoundary key="new">
        <ThrowError shouldThrow={false} />
      </ErrorBoundary>
    );
    
    expect(screen.getByText('Success')).toBeInTheDocument();
  });
});

// E2E测试
describe('ErrorBoundary E2E', () => {
  it('should handle production errors', async () => {
    await page.goto('http://localhost:3000');
    
    // 触发错误
    await page.click('#buggy-button');
    
    // 验证错误UI
    const errorMessage = await page.$eval(
      '.error-fallback',
      el => el.textContent
    );
    
    expect(errorMessage).toContain('出错了');
    
    // 验证错误上报
    const errorRequests = await page.evaluate(() => {
      return window.errorLog;
    });
    
    expect(errorRequests.length).toBeGreaterThan(0);
  });
});

4.2 调试技巧

javascript
// 开发环境调试
class DebugErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  componentDidCatch(error, errorInfo) {
    if (process.env.NODE_ENV === 'development') {
      // 详细的控制台输出
      console.group('🚨 Error Boundary Caught Error');
      console.error('Error:', error);
      console.error('Component Stack:', errorInfo.componentStack);
      console.error('Props:', this.props);
      console.error('State:', this.state);
      console.groupEnd();
      
      // 调试断点
      debugger;
    }
    
    // 生产环境上报
    if (process.env.NODE_ENV === 'production') {
      this.reportError(error, errorInfo);
    }
  }
  
  render() {
    if (this.state.hasError) {
      if (process.env.NODE_ENV === 'development') {
        return (
          <div className="dev-error-display">
            <h2>Development Error</h2>
            <pre>{this.state.error.stack}</pre>
          </div>
        );
      }
      
      return <ProductionErrorPage />;
    }
    
    return this.props.children;
  }
}

// 错误重现
class ReplayableErrorBoundary extends React.Component {
  actionsLog = [];
  
  componentDidMount() {
    // 记录所有用户操作
    window.addEventListener('click', this.logAction);
    window.addEventListener('input', this.logAction);
  }
  
  logAction = (e) => {
    this.actionsLog.push({
      type: e.type,
      target: e.target.tagName,
      timestamp: Date.now()
    });
  };
  
  componentDidCatch(error, errorInfo) {
    // 保存重现步骤
    localStorage.setItem('errorReplay', JSON.stringify({
      error: error.toString(),
      actions: this.actionsLog,
      state: captureAppState()
    }));
  }
  
  componentWillUnmount() {
    window.removeEventListener('click', this.logAction);
    window.removeEventListener('input', this.logAction);
  }
}

注意事项

1. 性能考虑

javascript
// ✅ 避免过多边界
function Optimized() {
  return (
    <ErrorBoundary>
      <ComponentGroup />  {/* 多个组件共享一个边界 */}
    </ErrorBoundary>
  );
}

function ComponentGroup() {
  return (
    <>
      <Component1 />
      <Component2 />
      <Component3 />
    </>
  );
}

// ❌ 过多独立边界
function NotOptimized() {
  return (
    <>
      <ErrorBoundary><Component1 /></ErrorBoundary>
      <ErrorBoundary><Component2 /></ErrorBoundary>
      <ErrorBoundary><Component3 /></ErrorBoundary>
    </>
  );
}

2. 安全考虑

javascript
// 过滤敏感信息
componentDidCatch(error, errorInfo) {
  const sanitizedError = {
    message: error.message.replace(/token=[^&]*/g, 'token=***'),
    stack: error.stack.replace(/password=[^&]*/g, 'password=***')
  };
  
  this.reportError(sanitizedError, errorInfo);
}

3. 用户隐私

javascript
// 遵守隐私政策
componentDidCatch(error, errorInfo) {
  // 获取用户同意
  if (getUserConsent()) {
    this.reportError(error, errorInfo);
  } else {
    // 仅本地记录
    console.error(error);
  }
}

常见问题

Q1: 应该在哪些地方添加错误边界?

A: 关键功能、第三方组件、动态内容、路由边界。

Q2: 错误边界会影响性能吗?

A: 几乎无影响,只在错误时有开销。

Q3: 如何测试错误边界?

A: 手动抛出错误、使用测试库模拟。

Q4: 生产和开发环境应该不同吗?

A: 是的,开发显示详情,生产友好提示。

Q5: 错误边界能捕获所有错误吗?

A: 不能,事件处理器和异步代码需单独处理。

Q6: 如何避免错误边界自身出错?

A: 外层包裹另一个边界,防御式编程。

Q7: 应该记录所有错误吗?

A: 根据严重程度,低优先级可选择性记录。

Q8: 如何处理频繁错误?

A: 实现限流、去重、聚合机制。

Q9: 错误恢复的最佳时机?

A: 用户主动重试或检测到条件改善时。

Q10: 如何平衡用户体验和调试需求?

A: 开发详细信息,生产友好提示加错误ID。

总结

核心原则

1. 设计原则
   ✅ 合理粒度
   ✅ 用户优先
   ✅ 功能降级
   ✅ 智能恢复

2. 实现要点
   ✅ 分层处理
   ✅ 环境区分
   ✅ 完整上下文
   ✅ 安全合规

3. 监控策略
   ✅ 分级上报
   ✅ 去重聚合
   ✅ 趋势分析
   ✅ 及时告警

实践清单

✅ 识别关键功能
✅ 设计边界粒度
✅ 实现通用边界
✅ 配置监控上报
✅ 设计fallback UI
✅ 实现恢复机制
✅ 编写测试用例
✅ 制定响应流程
✅ 定期审查优化

错误边界是React应用稳定性的基石,遵循最佳实践能显著提升应用质量和用户满意度。