From 420e0f660300d57f294fbadf8d30033f4d91470d Mon Sep 17 00:00:00 2001 From: qihu-bot Date: Tue, 11 Aug 2026 12:53:04 +0800 Subject: [PATCH] =?UTF-8?q?#962=20=E5=85=A8=E5=B1=80=20ErrorBoundary=20?= =?UTF-8?q?=E9=9A=94=E7=A6=BB=E7=BB=84=E4=BB=B6=E6=B8=B2=E6=9F=93=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main.tsx 加 ErrorBoundary 包裹 App,任何组件抛错都会显示在页面上而不是让 root 空白. 基于 #961 诊断:React 18 mount 正常,问题在 App.tsx 组件树. --- frontend/src/components/ErrorBoundary.tsx | 104 ++++++++++++++++++++++ frontend/src/main.tsx | 24 ++--- 2 files changed, 112 insertions(+), 16 deletions(-) create mode 100644 frontend/src/components/ErrorBoundary.tsx diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..cdd1870 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.tsx @@ -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 { + 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 ( +
+

❌ React 组件渲染错误

+

+ ErrorBoundary 捕获了渲染错误,防止整个应用崩溃。 +

+

错误信息

+
+            {error.name}: {error.message}
+          
+ {errorInfo && errorInfo.componentStack && ( + <> +

组件堆栈

+
+                {errorInfo.componentStack}
+              
+ + )} + +
+ ); + } + return this.props.children; + } +} + diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 5e90fa4..05c2b0f 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -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); - } + 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( - +ReactDOM.createRoot(rootEl).render( + + - - ); -} catch (err) { - console.error('[qihu-mount-error]', err); - rootEl.innerHTML = '
React mount failed: ' + String(err) + '
'; -} - + +
+);