React 中错误捕获的方式

#react

目录

总结

  • static getDerivedStateFromError
    • 更新 state,降级 UI 显示
  • componentDidCatch
    • 更后面,将错误日志上报给服务器
  • 全局错误
    • window.onerror
    • window.addEventListener(‘unhandledrejection’
    • window.addEventListener(‘error’
  • 通用的错误解决方案
    • 错误类型枚举
    • 错误上报
    • 默认错误回退组件等
    • 分层上报

更多参考

1. Error Boundary(错误边界)

这是 React 官方推荐的捕获渲染错误的方式:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null, errorInfo: null };
  }

  static getDerivedStateFromError(error) {
    // 更新 state,下次渲染时显示降级 UI
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // 可以在这里将错误日志上报给服务器
    console.error('Error:', error);
    console.error('Error Info:', errorInfo);
    this.setState({
      error,
      errorInfo
    });
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <details>
            <summary>Error Details</summary>
            <pre>{this.state.error && this.state.error.toString()}</pre>
            <pre>{this.state.errorInfo && this.state.errorInfo.componentStack}</pre>
          </details>
        </div>
      );
    }

    return this.props.children;
  }
}

2. 全局错误处理

// 在应用入口处设置
window.onerror = function(message, source, lineno, colno, error) {
  // 处理全局错误
  console.error('Global error:', { message, source, lineno, colno, error });
  // 上报错误
  return false;
};

// 处理 Promise 中的错误
window.addEventListener('unhandledrejection', function(event) {
  console.error('Unhandled promise rejection:', event.reason);
  // 上报错误
  event.preventDefault();
});

3. 异步错误处理

// 异步组件的错误处理
const AsyncComponent = React.lazy(() => import('./AsyncComponent'));

function SafeAsyncComponent() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<Loading />}>
        <AsyncComponent />
      </Suspense>
    </ErrorBoundary>
  );
}

// 异步请求错误处理
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data;
  } catch (error) {
    // 处理错误
    console.error('API Error:', error);
    throw error; // 或者返回默认值
  }
}

4. 通用错误处理解决方案

4.1. 创建错误类型枚举

// 1. 创建错误类型枚举
const ErrorTypes = {
  NETWORK_ERROR: 'NETWORK_ERROR',
  RENDER_ERROR: 'RENDER_ERROR',
  RUNTIME_ERROR: 'RUNTIME_ERROR',
  BUSINESS_ERROR: 'BUSINESS_ERROR'
};

4.2. 创建自定义错误类

// 2. 创建自定义错误类
class AppError extends Error {
  constructor(type, message, metadata = {}) {
    super(message);
    this.type = type;
    this.metadata = metadata;
    this.timestamp = new Date().toISOString();
  }
}

4.3. 错误处理服务:记得上报

// 3. 错误处理服务
class ErrorService {
  static async reportError(error, componentInfo = null) {
    const errorReport = {
      error: {
        message: error.message,
        stack: error.stack,
        type: error.type || 'UNKNOWN',
        metadata: error.metadata || {},
      },
      component: componentInfo,
      userInfo: {
        // 收集用户信息
        userId: localStorage.getItem('userId'),
        userAgent: navigator.userAgent,
        // 其他相关信息
      },
      timestamp: new Date().toISOString()
    };

    try {
      await fetch('/api/error-logging', {
        method: 'POST',
        body: JSON.stringify(errorReport)
      });
    } catch (e) {
      console.error('Failed to report error:', e);
      // 可以考虑使用备用报告机制,如 sendBeacon
      navigator.sendBeacon('/api/error-logging', JSON.stringify(errorReport));
    }
  }
  static getErrorMessage(error) {
    // 根据错误类型返回用户友好的错误信息
    const messages = {
      [ErrorTypes.NETWORK_ERROR]: '网络连接出现问题,请检查网络后重试',
      [ErrorTypes.RENDER_ERROR]: '页面渲染出现问题,请刷新重试',
      [ErrorTypes.BUSINESS_ERROR]: error.message,
      'default': '系统出现未知错误,请稍后重试'
    };

    return messages[error.type] || messages.default;
  }
}

4.4. 增强版错误边界组件

// 4. 增强版错误边界组件
class EnhancedErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      errorInfo: null
    };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    ErrorService.reportError(error, {
      componentStack: errorInfo.componentStack,
      componentName: this.props.name || 'Unknown'
    });
  }

  handleRetry = () => {
    this.setState({ hasError: false, error: null, errorInfo: null });
  };

  render() {
    if (this.state.hasError) {
      return this.props.fallback ? (
        this.props.fallback({
          error: this.state.error,
          retry: this.handleRetry
        })
      ) : (
        <DefaultErrorFallback
          error={this.state.error}
          onRetry={this.handleRetry}
        />
      );
    }

    return this.props.children;
  }
}

4.5. 默认错误回退组件

// 5. 默认错误回退组件
function DefaultErrorFallback({ error, onRetry }) {
  return (
    <div className="error-container">
      <h2>出错啦!</h2>
      <p>{ErrorService.getErrorMessage(error)}</p>
      <button onClick={onRetry}>重试</button>
    </div>
  );
}

4.6. 使用示例


// 6. 使用示例
function App() {
  return (
    <EnhancedErrorBoundary
      name="RootApp"
      fallback={({ error, retry }) => (
        <CustomErrorUI error={error} onRetry={retry} />
      )}
    >
      <Router>
        {/* 路由配置 */}
      </Router>
    </EnhancedErrorBoundary>
  );
}

// 7. 异步组件错误处理
const AsyncComponent = React.lazy(() => 
  import('./AsyncComponent')
    .catch(error => {
      ErrorService.reportError(
        new AppError(ErrorTypes.RUNTIME_ERROR, 'Failed to load component', {
          componentName: 'AsyncComponent'
        })
      );
      // 返回一个默认导出,避免应用崩溃
      return { default: () => <DefaultErrorFallback /> };
    })
);

// 8. API 请求错误处理
const api = {
  async fetch(url, options = {}) {
    try {
      const response = await fetch(url, options);
      
      if (!response.ok) {
        throw new AppError(
          ErrorTypes.NETWORK_ERROR,
          `HTTP error! status: ${response.status}`,
          { url, status: response.status }
        );
      }

      const data = await response.json();
      return data;
    } catch (error) {
      if (error instanceof AppError) {
        throw error;
      }
      
      throw new AppError(
        ErrorTypes.NETWORK_ERROR,
        'Failed to fetch data',
        { url, originalError: error.message }
      );
    }
  }
};

// 9. 在组件中使用
function DataComponent() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchData() {
      try {
        const result = await api.fetch('/api/data');
        setData(result);
      } catch (error) {
        setError(error);
        ErrorService.reportError(error, {
          componentName: 'DataComponent',
          action: 'fetchData'
        });
      }
    }

    fetchData();
  }, []);

  if (error) {
    return <ErrorMessage error={error} />;
  }

  if (!data) {
    return <Loading />;
  }

  return <div>{/* 渲染数据 */}</div>;
}

5. 使用建议

5.1. 在应用的关键节点使用 ErrorBoundary

function App() {
  return (
    <EnhancedErrorBoundary name="Root">
      <Router>
        <Switch>
          <Route path="/dashboard">
            <EnhancedErrorBoundary name="Dashboard">
              <Dashboard />
            </EnhancedErrorBoundary>
          </Route>
          {/* 其他路由 */}
        </Switch>
      </Router>
    </EnhancedErrorBoundary>
  );
}

5.2. 对于重要的异步操作添加错误处理

function ImportantComponent() {
  const handleImportantAction = async () => {
    try {
      await performAction();
    } catch (error) {
      ErrorService.reportError(
        new AppError(ErrorTypes.BUSINESS_ERROR, '操作失败', {
          action: 'importantAction'
        })
      );
      // 显示用户友好的错误消息
      message.error(ErrorService.getErrorMessage(error));
    }
  };

  return <button onClick={handleImportantAction}>执行操作</button>;
}

5.3. 在开发环境提供详细信息,生产环境注意安全

const ErrorMessage = ({ error }) => (
  <div className="error-message">
    <h3>{ErrorService.getErrorMessage(error)}</h3>
    {process.env.NODE_ENV === 'development' && (
      <details>
        <summary>Debug Information</summary>
        <pre>{JSON.stringify(error, null, 2)}</pre>
      </details>
    )}
  </div>
);

5.4. 分层错误处理

这种分层方式确保了即使某个部分出错,其他部分仍然可以正常工作

// App.js
function App() {
  return (
    <ErrorBoundary>
      <div className="app">
        <Header />
        <main>
          <ErrorBoundary>
            <Router>
              <Switch>
                <Route path="/dashboard">
                  <ErrorBoundary>
                    <Dashboard />
                  </ErrorBoundary>
                </Route>
                {/* 其他路由 */}
              </Switch>
            </Router>
          </ErrorBoundary>
        </main>
        <Footer />
      </div>
    </ErrorBoundary>
  );
}

5.5. 记得错误监控和上报

// 错误监控服务
const errorTracker = {
  init() {
    // 初始化错误追踪服务
  },

  captureError(error, context = {}) {
    // 收集错误信息
    const errorInfo = {
      message: error.message,
      stack: error.stack,
      timestamp: new Date().toISOString(),
      userInfo: getUserInfo(),
      context,
    };

    // 发送到服务器
    this.sendToServer(errorInfo);
  },

  sendToServer(errorInfo) {
    fetch('/api/errors', {
      method: 'POST',
      body: JSON.stringify(errorInfo),
    }).catch(err => {
      // 确保错误上报本身的错误不会影响应用
      console.error('Error reporting failed:', err);
    });
  }
};