Appearance
错误边界最佳实践
第一部分:设计原则
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应用稳定性的基石,遵循最佳实践能显著提升应用质量和用户满意度。