#962 全局 ErrorBoundary 隔离组件渲染错误

main.tsx 加 ErrorBoundary 包裹 App,任何组件抛错都会显示在页面上而不是让 root 空白. 基于 #961 诊断:React 18 mount 正常,问题在 App.tsx 组件树.
This commit is contained in:
qihu-bot committed 2026-08-11 12:53:04 +08:00
1 parent 4cf910ee3e
commit 420e0f6603
2 files changed
+107 -11

No files matched your search

+104
View File
@@ -0,0 +1,104 @@
import React from 'react';
interface State {
hasError: boolean;
error: Error | null;
errorInfo: React.ErrorInfo | null;
}
/**
* #962: 全局 ErrorBoundary — 隔离任何组件渲染错误
* - 防止单个组件抛错导致整个 React 树崩溃(root 空白)
* - 显示错误信息便于诊断
* - 提供 reset 按钮,方便恢复
*/
export default class ErrorBoundary extends React.Component<{ children: React.ReactNode }, State> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('[qihu-error-boundary]', error, errorInfo);
this.setState({ errorInfo });
}
reset = () => {
this.setState({ hasError: false, error: null, errorInfo: null });
// Hard reload to clear any bad state
if (typeof window !== 'undefined') {
window.location.href = window.location.pathname + window.location.search;
}
};
render() {
if (this.state.hasError && this.state.error) {
const { error, errorInfo } = this.state;
return (
<div style={{
padding: '32px',
margin: '32px auto',
maxWidth: '800px',
fontFamily: 'monospace',
background: '#fef2f2',
border: '2px solid #dc2626',
borderRadius: '8px',
color: '#1a1a1a',
}}>
<h1 style={{ color: '#dc2626', marginTop: 0 }}>❌ React 组件渲染错误</h1>
<p style={{ color: '#666', marginBottom: '16px' }}>
ErrorBoundary 捕获了渲染错误,防止整个应用崩溃。
</p>
<h3>错误信息</h3>
<pre style={{
background: '#fff',
padding: '12px',
borderRadius: '4px',
overflow: 'auto',
fontSize: '13px',
border: '1px solid #fca5a5',
}}>
{error.name}: {error.message}
</pre>
{errorInfo && errorInfo.componentStack && (
<>
<h3>组件堆栈</h3>
<pre style={{
background: '#fff',
padding: '12px',
borderRadius: '4px',
overflow: 'auto',
fontSize: '11px',
border: '1px solid #fca5a5',
maxHeight: '300px',
}}>
{errorInfo.componentStack}
</pre>
</>
)}
<button
onClick={this.reset}
style={{
marginTop: '16px',
padding: '10px 24px',
background: '#5B5FED',
color: '#fff',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px',
}}
>
🔄 重置页面
</button>
</div>
);
}
return this.props.children;
}
}
+3 -11
View File
@@ -2,34 +2,26 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import ErrorBoundary from './components/ErrorBoundary';
import './styles/global.css';
// #961: 全局错误处理 + 防止 hydration mismatch
window.addEventListener('error', (e) => {
const root = document.getElementById('root');
if (root && e.error) {
console.error('[qihu-global-error]', e.error);
}
});
window.addEventListener('unhandledrejection', (e) => {
console.error('[qihu-unhandled-rejection]', e.reason);
});
const rootEl = document.getElementById('root')!;
// 清空避免 hydration mismatch
rootEl.innerHTML = '';
try {
ReactDOM.createRoot(rootEl).render(
<React.StrictMode>
<ErrorBoundary>
<BrowserRouter>
<App />
</BrowserRouter>
</ErrorBoundary>
</React.StrictMode>
);
} catch (err) {
console.error('[qihu-mount-error]', err);
rootEl.innerHTML = '<pre style="padding:20px;color:#dc2626;font-family:monospace">React mount failed: ' + String(err) + '</pre>';
}