#962 全局 ErrorBoundary 隔离组件渲染错误
main.tsx 加 ErrorBoundary 包裹 App,任何组件抛错都会显示在页面上而不是让 root 空白. 基于 #961 诊断:React 18 mount 正常,问题在 App.tsx 组件树.
This commit is contained in:
1 parent
4cf910ee3e
commit
420e0f6603
2 files changed
+112
-16
No files matched your search
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+8
-16
@@ -2,34 +2,26 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import ErrorBoundary from './components/ErrorBoundary';
|
||||||
import './styles/global.css';
|
import './styles/global.css';
|
||||||
|
|
||||||
// #961: 全局错误处理 + 防止 hydration mismatch
|
|
||||||
window.addEventListener('error', (e) => {
|
window.addEventListener('error', (e) => {
|
||||||
const root = document.getElementById('root');
|
console.error('[qihu-global-error]', e.error);
|
||||||
if (root && e.error) {
|
|
||||||
console.error('[qihu-global-error]', e.error);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
window.addEventListener('unhandledrejection', (e) => {
|
window.addEventListener('unhandledrejection', (e) => {
|
||||||
console.error('[qihu-unhandled-rejection]', e.reason);
|
console.error('[qihu-unhandled-rejection]', e.reason);
|
||||||
});
|
});
|
||||||
|
|
||||||
const rootEl = document.getElementById('root')!;
|
const rootEl = document.getElementById('root')!;
|
||||||
// 清空避免 hydration mismatch
|
|
||||||
rootEl.innerHTML = '';
|
rootEl.innerHTML = '';
|
||||||
|
|
||||||
try {
|
ReactDOM.createRoot(rootEl).render(
|
||||||
ReactDOM.createRoot(rootEl).render(
|
<React.StrictMode>
|
||||||
<React.StrictMode>
|
<ErrorBoundary>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</React.StrictMode>
|
</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>';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Reference in new issue
Block a user