Appearance
路由错误处理
概述
React Router v6提供了强大的错误处理机制,允许在路由层面捕获和处理各种错误。通过errorElement和useRouteError等API,可以创建优雅的错误边界,提供良好的错误恢复体验。本文深入探讨路由错误处理的各种模式和最佳实践。
基础错误处理
errorElement基础
jsx
import { createBrowserRouter, useRouteError, isRouteErrorResponse } from 'react-router-dom';
// 基础错误边界组件
function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
// 这是由loader或action主动抛出的Response错误
return (
<div className="error-page">
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</div>
);
}
// 未预期的错误
return (
<div className="error-page">
<h1>Oops! Something went wrong</h1>
<p>{error.message || 'Unknown error occurred'}</p>
{process.env.NODE_ENV === 'development' && (
<pre className="error-stack">{error.stack}</pre>
)}
</div>
);
}
// 路由配置with错误处理
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
errorElement: <ErrorBoundary />,
children: [
{
index: true,
element: <Home />
},
{
path: 'users/:userId',
element: <UserProfile />,
loader: userLoader,
errorElement: <UserErrorBoundary />
}
]
}
]);
// Loader中抛出错误
async function userLoader({ params }) {
const { userId } = params;
const response = await fetch(`/api/users/${userId}`);
if (response.status === 404) {
throw new Response('User not found', { status: 404 });
}
if (response.status === 403) {
throw new Response('Access forbidden', { status: 403 });
}
if (!response.ok) {
throw new Response('Failed to load user', { status: 500 });
}
return response.json();
}
// 用户特定的错误边界
function UserErrorBoundary() {
const error = useRouteError();
const navigate = useNavigate();
if (isRouteErrorResponse(error)) {
if (error.status === 404) {
return (
<div className="error-page">
<h1>User Not Found</h1>
<p>The user you're looking for doesn't exist.</p>
<button onClick={() => navigate('/users')}>
View All Users
</button>
</div>
);
}
if (error.status === 403) {
return (
<div className="error-page">
<h1>Access Denied</h1>
<p>You don't have permission to view this user's profile.</p>
<button onClick={() => navigate('/')}>
Go Home
</button>
</div>
);
}
}
return (
<div className="error-page">
<h1>Error Loading User</h1>
<p>Something went wrong while loading the user profile.</p>
<button onClick={() => navigate(0)}>
Try Again
</button>
<button onClick={() => navigate('/users')}>
Back to Users
</button>
</div>
);
}嵌套错误边界
jsx
// 多层错误边界
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
errorElement: <RootErrorBoundary />,
children: [
{
index: true,
element: <Home />
},
{
path: 'dashboard',
element: <DashboardLayout />,
errorElement: <DashboardErrorBoundary />,
children: [
{
index: true,
element: <DashboardHome />
},
{
path: 'analytics',
element: <Analytics />,
loader: analyticsLoader,
errorElement: <AnalyticsErrorBoundary />
},
{
path: 'settings',
element: <Settings />,
loader: settingsLoader,
errorElement: <SettingsErrorBoundary />
}
]
}
]
}
]);
// 根级错误边界 - 处理最严重的错误
function RootErrorBoundary() {
const error = useRouteError();
return (
<div className="root-error">
<div className="error-container">
<h1>Application Error</h1>
<p>We're sorry, but something went wrong with the application.</p>
{isRouteErrorResponse(error) && (
<div className="error-details">
<p>Status: {error.status}</p>
<p>Message: {error.statusText}</p>
</div>
)}
<div className="error-actions">
<button onClick={() => window.location.href = '/'}>
Return to Home
</button>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
{process.env.NODE_ENV === 'development' && error.stack && (
<details className="error-stack">
<summary>Error Stack Trace</summary>
<pre>{error.stack}</pre>
</details>
)}
</div>
</div>
);
}
// 仪表板错误边界 - 保留导航
function DashboardErrorBoundary() {
const error = useRouteError();
return (
<div className="dashboard-error">
<aside className="dashboard-sidebar">
<DashboardNavigation />
</aside>
<main className="dashboard-content">
<div className="error-message">
<h2>Dashboard Error</h2>
<p>An error occurred while loading this section of the dashboard.</p>
{isRouteErrorResponse(error) && (
<p className="error-detail">{error.data}</p>
)}
<div className="error-actions">
<Link to="/dashboard">Go to Dashboard Home</Link>
<button onClick={() => window.location.reload()}>
Try Again
</button>
</div>
</div>
</main>
</div>
);
}
// 分析页面错误边界 - 最具体的错误处理
function AnalyticsErrorBoundary() {
const error = useRouteError();
const navigate = useNavigate();
return (
<div className="analytics-error">
<h2>Analytics Error</h2>
<p>Unable to load analytics data.</p>
{isRouteErrorResponse(error) && error.status === 403 && (
<div className="permission-error">
<p>You don't have permission to view analytics.</p>
<Link to="/dashboard/settings">
Request Access
</Link>
</div>
)}
{isRouteErrorResponse(error) && error.status === 500 && (
<div className="server-error">
<p>Our analytics service is temporarily unavailable.</p>
<button onClick={() => navigate(0)}>Retry</button>
</div>
)}
<Link to="/dashboard">Back to Dashboard</Link>
</div>
);
}错误类型处理
HTTP状态码错误
jsx
// 统一的HTTP错误处理
class HTTPError extends Error {
constructor(status, statusText, data) {
super(statusText);
this.status = status;
this.statusText = statusText;
this.data = data;
}
}
// Loader中创建特定错误
async function resourceLoader({ params }) {
const response = await fetch(`/api/resources/${params.id}`);
switch (response.status) {
case 404:
throw new Response('Resource not found', {
status: 404,
statusText: 'Not Found'
});
case 401:
throw new Response('Please log in to continue', {
status: 401,
statusText: 'Unauthorized'
});
case 403:
throw new Response('You do not have access to this resource', {
status: 403,
statusText: 'Forbidden'
});
case 429:
throw new Response('Too many requests. Please try again later.', {
status: 429,
statusText: 'Too Many Requests'
});
case 503:
throw new Response('Service temporarily unavailable', {
status: 503,
statusText: 'Service Unavailable'
});
default:
if (!response.ok) {
throw new Response('An unexpected error occurred', {
status: response.status,
statusText: 'Server Error'
});
}
}
return response.json();
}
// 状态码特定的错误处理
function HTTPErrorBoundary() {
const error = useRouteError();
const navigate = useNavigate();
const location = useLocation();
if (!isRouteErrorResponse(error)) {
return <GenericErrorPage error={error} />;
}
const errorHandlers = {
404: () => (
<div className="error-404">
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<div className="error-suggestions">
<h3>You might want to:</h3>
<ul>
<li><Link to="/">Go to homepage</Link></li>
<li><button onClick={() => navigate(-1)}>Go back</button></li>
<li><Link to="/search">Search our site</Link></li>
</ul>
</div>
</div>
),
401: () => (
<div className="error-401">
<h1>Authentication Required</h1>
<p>{error.data}</p>
<button onClick={() => {
navigate('/login', {
state: { from: location.pathname }
});
}}>
Sign In
</button>
</div>
),
403: () => (
<div className="error-403">
<h1>Access Denied</h1>
<p>{error.data}</p>
<div className="error-actions">
<Link to="/">Go Home</Link>
<Link to="/contact">Contact Support</Link>
</div>
</div>
),
429: () => (
<div className="error-429">
<h1>Too Many Requests</h1>
<p>You've made too many requests. Please wait a moment before trying again.</p>
<RateLimitTimer onExpire={() => navigate(0)} />
</div>
),
500: () => (
<div className="error-500">
<h1>Server Error</h1>
<p>Something went wrong on our end. We're working to fix it.</p>
<button onClick={() => navigate(0)}>Try Again</button>
</div>
),
503: () => (
<div className="error-503">
<h1>Service Unavailable</h1>
<p>Our service is temporarily down for maintenance.</p>
<p>Please check back in a few minutes.</p>
<ServiceStatusChecker />
</div>
)
};
const ErrorComponent = errorHandlers[error.status] || (() => (
<div className="error-generic">
<h1>{error.status} - {error.statusText}</h1>
<p>{error.data}</p>
<button onClick={() => navigate(-1)}>Go Back</button>
</div>
));
return <ErrorComponent />;
}
// 速率限制计时器组件
function RateLimitTimer({ onExpire }) {
const [remaining, setRemaining] = useState(60);
useEffect(() => {
const timer = setInterval(() => {
setRemaining(prev => {
if (prev <= 1) {
clearInterval(timer);
onExpire();
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}, [onExpire]);
return (
<div className="rate-limit-timer">
<p>You can try again in {remaining} seconds</p>
</div>
);
}
// 服务状态检查器
function ServiceStatusChecker() {
const [status, setStatus] = useState('checking');
useEffect(() => {
const checkStatus = async () => {
try {
const response = await fetch('/api/health');
if (response.ok) {
setStatus('available');
} else {
setStatus('unavailable');
}
} catch {
setStatus('unavailable');
}
};
const interval = setInterval(checkStatus, 10000); // 每10秒检查一次
checkStatus();
return () => clearInterval(interval);
}, []);
if (status === 'available') {
return (
<div className="service-available">
<p>Service is back online!</p>
<button onClick={() => window.location.reload()}>
Refresh Page
</button>
</div>
);
}
return (
<div className="service-checking">
<p>Checking service status...</p>
</div>
);
}网络错误处理
jsx
// 网络错误检测和处理
async function networkAwareLoader({ params }) {
try {
const response = await fetch(`/api/data/${params.id}`);
if (!response.ok) {
throw new Response('Failed to fetch data', {
status: response.status
});
}
return response.json();
} catch (error) {
// 网络错误
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new Response('Network error. Please check your internet connection.', {
status: 0, // 特殊状态码表示网络错误
statusText: 'Network Error'
});
}
// 超时错误
if (error.name === 'AbortError') {
throw new Response('Request timed out. Please try again.', {
status: 0,
statusText: 'Timeout'
});
}
throw error;
}
}
// 网络错误边界
function NetworkErrorBoundary() {
const error = useRouteError();
const [isOnline, setIsOnline] = useState(navigator.onLine);
const navigate = useNavigate();
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
if (isRouteErrorResponse(error) && error.status === 0) {
return (
<div className="network-error">
<div className="error-icon">
{isOnline ? '⚠️' : '📡'}
</div>
<h1>
{isOnline ? 'Connection Problem' : 'You are offline'}
</h1>
<p>
{isOnline
? 'Unable to connect to the server. Please check your connection and try again.'
: 'Please check your internet connection and try again.'
}
</p>
<div className="connection-status">
<div className={`status-indicator ${isOnline ? 'online' : 'offline'}`} />
<span>{isOnline ? 'Connected' : 'No connection'}</span>
</div>
<div className="error-actions">
<button onClick={() => navigate(0)} disabled={!isOnline}>
Try Again
</button>
<button onClick={() => navigate(-1)}>
Go Back
</button>
</div>
<NetworkDiagnostics />
</div>
);
}
return <DefaultErrorBoundary />;
}
// 网络诊断组件
function NetworkDiagnostics() {
const [diagnostics, setDiagnostics] = useState(null);
const runDiagnostics = async () => {
const results = {
onlineStatus: navigator.onLine,
connectionType: navigator.connection?.effectiveType || 'unknown',
downlink: navigator.connection?.downlink || 'unknown',
rtt: navigator.connection?.rtt || 'unknown'
};
// 测试DNS
try {
await fetch('https://dns.google/resolve?name=example.com', {
mode: 'no-cors'
});
results.dns = 'OK';
} catch {
results.dns = 'Failed';
}
// 测试API连接
try {
await fetch('/api/health', {
method: 'HEAD'
});
results.api = 'OK';
} catch {
results.api = 'Failed';
}
setDiagnostics(results);
};
return (
<div className="network-diagnostics">
<button onClick={runDiagnostics}>
Run Network Diagnostics
</button>
{diagnostics && (
<div className="diagnostics-results">
<h3>Diagnostics Results:</h3>
<ul>
<li>Online Status: {diagnostics.onlineStatus ? 'Online' : 'Offline'}</li>
<li>Connection Type: {diagnostics.connectionType}</li>
<li>Download Speed: {diagnostics.downlink} Mbps</li>
<li>Latency: {diagnostics.rtt} ms</li>
<li>DNS: {diagnostics.dns}</li>
<li>API Server: {diagnostics.api}</li>
</ul>
</div>
)}
</div>
);
}权限错误处理
jsx
// 权限错误类
class PermissionError extends Error {
constructor(message, requiredPermission, userPermissions) {
super(message);
this.name = 'PermissionError';
this.requiredPermission = requiredPermission;
this.userPermissions = userPermissions;
}
}
// 带权限检查的Loader
async function protectedLoader({ params, context }) {
const { user, permissions } = context;
// 检查认证
if (!user) {
throw new Response('Authentication required', {
status: 401,
statusText: 'Unauthorized'
});
}
// 检查权限
const requiredPermission = 'resource:read';
if (!permissions.includes(requiredPermission)) {
throw new PermissionError(
'You do not have permission to view this resource',
requiredPermission,
permissions
);
}
// 加载数据
const data = await fetch(`/api/resources/${params.id}`)
.then(r => r.json());
return data;
}
// 权限错误边界
function PermissionErrorBoundary() {
const error = useRouteError();
const { user } = useAuth();
const navigate = useNavigate();
// 处理PermissionError
if (error instanceof PermissionError) {
return (
<div className="permission-error">
<h1>Access Denied</h1>
<p>{error.message}</p>
<div className="permission-details">
<h3>Permission Required:</h3>
<code>{error.requiredPermission}</code>
<h3>Your Permissions:</h3>
<ul>
{error.userPermissions.map(perm => (
<li key={perm}><code>{perm}</code></li>
))}
</ul>
</div>
<div className="error-actions">
<button onClick={() => navigate('/')}>
Go Home
</button>
<Link to="/upgrade">
Upgrade Your Account
</Link>
<Link to="/contact">
Request Access
</Link>
</div>
</div>
);
}
// 处理401错误
if (isRouteErrorResponse(error) && error.status === 401) {
return (
<div className="auth-required">
<h1>Sign In Required</h1>
<p>Please sign in to access this page.</p>
<button onClick={() => {
navigate('/login', {
state: { from: location.pathname }
});
}}>
Sign In
</button>
<p>
Don't have an account? <Link to="/register">Sign up</Link>
</p>
</div>
);
}
return <DefaultErrorBoundary />;
}错误恢复策略
自动重试
jsx
// 带重试的Loader
async function retryableLoader({ params }, retryCount = 0) {
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000;
try {
const response = await fetch(`/api/data/${params.id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
} catch (error) {
if (retryCount < MAX_RETRIES) {
console.log(`Retry attempt ${retryCount + 1} of ${MAX_RETRIES}`);
// 指数退避
const delay = RETRY_DELAY * Math.pow(2, retryCount);
await new Promise(resolve => setTimeout(resolve, delay));
return retryableLoader({ params }, retryCount + 1);
}
// 达到最大重试次数
throw new Response(
`Failed to load data after ${MAX_RETRIES} attempts`,
{ status: 500 }
);
}
}
// 带重试UI的错误边界
function RetryableErrorBoundary() {
const error = useRouteError();
const navigate = useNavigate();
const [retrying, setRetrying] = useState(false);
const [retryCount, setRetryCount] = useState(0);
const handleRetry = async () => {
setRetrying(true);
setRetryCount(prev => prev + 1);
// 延迟后重试
await new Promise(resolve => setTimeout(resolve, 1000));
navigate(0); // 重新加载当前路由
};
return (
<div className="retryable-error">
<h1>Something went wrong</h1>
<p>{error.data || error.message}</p>
{retryCount > 0 && (
<p className="retry-info">
Retry attempts: {retryCount}
</p>
)}
<div className="error-actions">
<button
onClick={handleRetry}
disabled={retrying}
>
{retrying ? 'Retrying...' : 'Try Again'}
</button>
<button onClick={() => navigate(-1)}>
Go Back
</button>
{retryCount >= 3 && (
<Link to="/support">
Contact Support
</Link>
)}
</div>
{retrying && (
<div className="retry-progress">
<div className="spinner" />
<p>Retrying...</p>
</div>
)}
</div>
);
}降级处理
jsx
// 带降级的Loader
async function fallbackLoader({ params }) {
try {
// 尝试从主API加载
const response = await fetch(`/api/v2/data/${params.id}`);
if (!response.ok) {
throw new Error('Primary API failed');
}
return {
data: await response.json(),
source: 'primary'
};
} catch (primaryError) {
console.warn('Primary API failed, trying fallback...', primaryError);
try {
// 尝试从备用API加载
const fallbackResponse = await fetch(`/api/v1/data/${params.id}`);
if (!fallbackResponse.ok) {
throw new Error('Fallback API failed');
}
return {
data: await fallbackResponse.json(),
source: 'fallback',
warning: 'Using older API version'
};
} catch (fallbackError) {
console.error('Both APIs failed', fallbackError);
// 尝试从缓存加载
const cached = getCachedData(params.id);
if (cached) {
return {
data: cached,
source: 'cache',
warning: 'Showing cached data - may be outdated'
};
}
// 所有选项都失败了
throw new Response(
'Unable to load data from any source',
{ status: 503 }
);
}
}
}
// 显示降级警告的组件
function DataView() {
const loaderData = useLoaderData();
const { data, source, warning } = loaderData;
return (
<div className="data-view">
{warning && (
<div className="degraded-service-warning">
<strong>Notice:</strong> {warning}
{source === 'cache' && (
<button onClick={() => window.location.reload()}>
Try to Refresh
</button>
)}
</div>
)}
<div className="data-content">
{/* 渲染数据 */}
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
{source !== 'primary' && (
<div className="data-source-info">
Data source: {source}
</div>
)}
</div>
);
}部分失败处理
jsx
// 处理部分数据加载失败
async function partialFailureLoader({ params }) {
const results = {
critical: null,
optional: [],
errors: []
};
// 关键数据必须成功
try {
results.critical = await fetch(`/api/critical/${params.id}`)
.then(r => r.json());
} catch (error) {
throw new Response('Failed to load critical data', { status: 500 });
}
// 可选数据允许失败
const optionalPromises = [
fetch(`/api/optional1/${params.id}`).then(r => r.json()),
fetch(`/api/optional2/${params.id}`).then(r => r.json()),
fetch(`/api/optional3/${params.id}`).then(r => r.json())
];
const optionalResults = await Promise.allSettled(optionalPromises);
optionalResults.forEach((result, index) => {
if (result.status === 'fulfilled') {
results.optional.push(result.value);
} else {
results.errors.push({
source: `optional${index + 1}`,
error: result.reason.message
});
}
});
return results;
}
// 显示部分失败的组件
function PartialDataView() {
const { critical, optional, errors } = useLoaderData();
return (
<div className="partial-data-view">
{/* 关键数据总是显示 */}
<section className="critical-section">
<h2>Main Content</h2>
<div>{critical.content}</div>
</section>
{/* 可选数据 */}
{optional.length > 0 && (
<section className="optional-section">
<h2>Additional Information</h2>
{optional.map((item, index) => (
<div key={index}>{item.content}</div>
))}
</section>
)}
{/* 错误提示 */}
{errors.length > 0 && (
<div className="partial-errors">
<p>Some content could not be loaded:</p>
<ul>
{errors.map((error, index) => (
<li key={index}>
{error.source}: {error.error}
</li>
))}
</ul>
</div>
)}
</div>
);
}错误报告和监控
错误日志记录
jsx
// 错误日志服务
class ErrorLogger {
constructor() {
this.logs = [];
this.maxLogs = 100;
}
log(error, context = {}) {
const logEntry = {
timestamp: new Date().toISOString(),
error: {
message: error.message,
stack: error.stack,
name: error.name
},
context: {
url: window.location.href,
userAgent: navigator.userAgent,
...context
}
};
this.logs.push(logEntry);
// 限制日志数量
if (this.logs.length > this.maxLogs) {
this.logs.shift();
}
// 发送到服务器
this.sendToServer(logEntry);
return logEntry;
}
async sendToServer(logEntry) {
try {
await fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(logEntry)
});
} catch (error) {
console.error('Failed to send error log:', error);
}
}
getLogs() {
return [...this.logs];
}
clearLogs() {
this.logs = [];
}
}
const errorLogger = new ErrorLogger();
// 带日志的错误边界
function LoggingErrorBoundary() {
const error = useRouteError();
const location = useLocation();
const { user } = useAuth();
useEffect(() => {
// 记录错误
errorLogger.log(error, {
route: location.pathname,
userId: user?.id,
userRole: user?.role,
isRouteError: isRouteErrorResponse(error),
status: isRouteErrorResponse(error) ? error.status : undefined
});
}, [error, location, user]);
return (
<div className="error-page">
<h1>Something went wrong</h1>
<p>We've been notified and are working on a fix.</p>
{process.env.NODE_ENV === 'development' && (
<details className="error-details">
<summary>Error Details</summary>
<pre>{JSON.stringify({
message: error.message,
stack: error.stack,
location: location.pathname
}, null, 2)}</pre>
</details>
)}
<button onClick={() => window.location.reload()}>
Try Again
</button>
</div>
);
}集成第三方错误监控
jsx
// Sentry集成
import * as Sentry from '@sentry/react';
// 初始化Sentry
Sentry.init({
dsn: process.env.REACT_APP_SENTRY_DSN,
environment: process.env.NODE_ENV,
beforeSend(event, hint) {
// 过滤敏感信息
if (event.user) {
delete event.user.email;
delete event.user.ip_address;
}
return event;
}
});
// Sentry错误边界
function SentryErrorBoundary() {
const error = useRouteError();
const location = useLocation();
useEffect(() => {
// 发送错误到Sentry
Sentry.captureException(error, {
contexts: {
router: {
location: location.pathname,
search: location.search
}
},
tags: {
errorType: isRouteErrorResponse(error) ? 'route' : 'runtime',
status: isRouteErrorResponse(error) ? error.status : undefined
}
});
}, [error, location]);
return (
<div className="error-page">
<h1>Oops! Something went wrong</h1>
<p>Our team has been notified and is working on a fix.</p>
<button onClick={() => {
Sentry.showReportDialog({
eventId: Sentry.lastEventId(),
user: {
name: 'User',
email: ''
}
});
}}>
Report Feedback
</button>
</div>
);
}
// 自定义错误监控
class ErrorMonitor {
constructor() {
this.errors = [];
this.listeners = [];
}
capture(error, context = {}) {
const errorData = {
id: Date.now(),
timestamp: new Date().toISOString(),
error: {
message: error.message,
stack: error.stack,
name: error.name
},
context,
userAgent: navigator.userAgent,
url: window.location.href
};
this.errors.push(errorData);
this.notifyListeners(errorData);
// 发送到监控服务
this.sendToMonitoring(errorData);
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
notifyListeners(errorData) {
this.listeners.forEach(listener => {
try {
listener(errorData);
} catch (error) {
console.error('Error in error listener:', error);
}
});
}
async sendToMonitoring(errorData) {
try {
await fetch('/api/monitoring/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorData)
});
} catch (error) {
console.error('Failed to send error to monitoring:', error);
}
}
getErrors() {
return [...this.errors];
}
clearErrors() {
this.errors = [];
}
}
const errorMonitor = new ErrorMonitor();
// 错误监控Hook
function useErrorMonitoring() {
const [errors, setErrors] = useState([]);
useEffect(() => {
const unsubscribe = errorMonitor.subscribe((errorData) => {
setErrors(prev => [...prev, errorData].slice(-10)); // 保留最新10个错误
});
return unsubscribe;
}, []);
return {
errors,
clearErrors: () => {
setErrors([]);
errorMonitor.clearErrors();
}
};
}
// 错误监控面板
function ErrorMonitoringPanel() {
const { errors, clearErrors } = useErrorMonitoring();
if (process.env.NODE_ENV !== 'development') {
return null;
}
return (
<div className="error-monitoring-panel">
<div className="panel-header">
<h3>Error Monitor ({errors.length})</h3>
<button onClick={clearErrors}>Clear</button>
</div>
<div className="errors-list">
{errors.map(error => (
<div key={error.id} className="error-item">
<div className="error-time">
{new Date(error.timestamp).toLocaleTimeString()}
</div>
<div className="error-message">
{error.error.message}
</div>
<div className="error-location">
{error.context.route}
</div>
</div>
))}
</div>
</div>
);
}用户友好的错误页面
交互式错误页面
jsx
// 丰富的错误页面组件
function RichErrorPage() {
const error = useRouteError();
const navigate = useNavigate();
const [feedback, setFeedback] = useState('');
const [submitted, setSubmitted] = useState(false);
const handleSubmitFeedback = async () => {
try {
await fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: error.message,
feedback,
url: window.location.href,
timestamp: new Date().toISOString()
})
});
setSubmitted(true);
setTimeout(() => setSubmitted(false), 3000);
} catch (error) {
console.error('Failed to submit feedback:', error);
}
};
return (
<div className="rich-error-page">
<div className="error-container">
<div className="error-icon">
<ErrorIllustration type={getErrorType(error)} />
</div>
<h1>Oops! Something went wrong</h1>
<p className="error-message">
{getErrorMessage(error)}
</p>
{/* 建议的操作 */}
<div className="suggested-actions">
<h3>You can try:</h3>
<div className="actions-grid">
<button
className="action-card"
onClick={() => navigate(0)}
>
<span className="action-icon">🔄</span>
<span className="action-text">Refresh the page</span>
</button>
<button
className="action-card"
onClick={() => navigate(-1)}
>
<span className="action-icon">⬅️</span>
<span className="action-text">Go back</span>
</button>
<button
className="action-card"
onClick={() => navigate('/')}
>
<span className="action-icon">🏠</span>
<span className="action-text">Go to homepage</span>
</button>
<Link to="/help" className="action-card">
<span className="action-icon">❓</span>
<span className="action-text">Visit help center</span>
</Link>
</div>
</div>
{/* 反馈表单 */}
<div className="error-feedback">
<h3>Help us improve</h3>
<p>Let us know what happened:</p>
<textarea
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
placeholder="Describe what you were trying to do..."
rows="4"
/>
<button
onClick={handleSubmitFeedback}
disabled={!feedback || submitted}
>
{submitted ? 'Thank you!' : 'Send Feedback'}
</button>
</div>
{/* 常见问题 */}
<div className="error-faq">
<h3>Common questions:</h3>
<details>
<summary>Why did this happen?</summary>
<p>This error occurred because {getErrorExplanation(error)}</p>
</details>
<details>
<summary>Will my data be lost?</summary>
<p>No, your data is safe. We automatically save your work.</p>
</details>
<details>
<summary>How long will this take to fix?</summary>
<p>Our team has been notified and is working on a solution.</p>
</details>
</div>
</div>
</div>
);
}
// 错误类型图标
function ErrorIllustration({ type }) {
const illustrations = {
'404': '🔍',
'403': '🔒',
'500': '🔧',
'network': '📡',
'default': '⚠️'
};
return (
<div className="error-illustration">
{illustrations[type] || illustrations.default}
</div>
);
}
// 获取用户友好的错误信息
function getErrorMessage(error) {
if (isRouteErrorResponse(error)) {
const messages = {
404: "We couldn't find the page you're looking for.",
403: "You don't have permission to access this page.",
401: "Please sign in to continue.",
500: "Something went wrong on our end.",
503: "This service is temporarily unavailable."
};
return messages[error.status] || error.data;
}
return "An unexpected error occurred. Please try again.";
}
// 获取错误类型
function getErrorType(error) {
if (isRouteErrorResponse(error)) {
return error.status.toString();
}
if (error.message?.includes('network')) {
return 'network';
}
return 'default';
}
// 获取错误解释
function getErrorExplanation(error) {
if (isRouteErrorResponse(error)) {
const explanations = {
404: "the page you requested doesn't exist or has been moved.",
403: "you don't have the necessary permissions.",
500: "there was a problem on our server.",
503: "our service is temporarily down for maintenance."
};
return explanations[error.status] || "of an unexpected issue.";
}
return "of an unexpected technical issue.";
}品牌化错误页面
jsx
// 品牌化的404页面
function Custom404Page() {
const navigate = useNavigate();
const [searchQuery, setSearchQuery] = useState('');
const handleSearch = (e) => {
e.preventDefault();
if (searchQuery.trim()) {
navigate(`/search?q=${encodeURIComponent(searchQuery)}`);
}
};
return (
<div className="custom-404">
<div className="error-content">
<div className="error-animation">
<Lost404Animation />
</div>
<h1>Page Not Found</h1>
<p className="error-subtitle">
Looks like you've ventured into uncharted territory!
</p>
{/* 搜索栏 */}
<form onSubmit={handleSearch} className="error-search">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="What are you looking for?"
/>
<button type="submit">Search</button>
</form>
{/* 热门链接 */}
<div className="popular-links">
<h3>Popular pages:</h3>
<div className="links-grid">
<Link to="/" className="link-card">
<span>🏠</span>
<span>Home</span>
</Link>
<Link to="/products" className="link-card">
<span>🛍️</span>
<span>Products</span>
</Link>
<Link to="/about" className="link-card">
<span>ℹ️</span>
<span>About</span>
</Link>
<Link to="/contact" className="link-card">
<span>📧</span>
<span>Contact</span>
</Link>
</div>
</div>
</div>
</div>
);
}
// 品牌化的维护页面
function MaintenancePage() {
const [timeRemaining, setTimeRemaining] = useState(null);
useEffect(() => {
// 获取维护结束时间
fetch('/api/maintenance/status')
.then(r => r.json())
.then(data => {
if (data.endTime) {
const end = new Date(data.endTime);
updateCountdown(end);
const interval = setInterval(() => {
updateCountdown(end);
}, 1000);
return () => clearInterval(interval);
}
});
}, []);
const updateCountdown = (endTime) => {
const now = new Date();
const diff = endTime - now;
if (diff <= 0) {
window.location.reload();
return;
}
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
setTimeRemaining({ hours, minutes, seconds });
};
return (
<div className="maintenance-page">
<div className="maintenance-content">
<MaintenanceAnimation />
<h1>We'll be right back!</h1>
<p>We're making some improvements to serve you better.</p>
{timeRemaining && (
<div className="countdown">
<div className="countdown-item">
<span className="countdown-value">{timeRemaining.hours}</span>
<span className="countdown-label">Hours</span>
</div>
<div className="countdown-item">
<span className="countdown-value">{timeRemaining.minutes}</span>
<span className="countdown-label">Minutes</span>
</div>
<div className="countdown-item">
<span className="countdown-value">{timeRemaining.seconds}</span>
<span className="countdown-label">Seconds</span>
</div>
</div>
)}
<div className="maintenance-info">
<p>In the meantime:</p>
<ul>
<li>Follow us on social media for updates</li>
<li>Check our status page for real-time information</li>
<li>Contact support if you need immediate assistance</li>
</ul>
</div>
<div className="social-links">
<a href="https://twitter.com/yourapp" target="_blank" rel="noopener noreferrer">
Twitter
</a>
<a href="https://facebook.com/yourapp" target="_blank" rel="noopener noreferrer">
Facebook
</a>
<a href="https://status.yourapp.com" target="_blank" rel="noopener noreferrer">
Status Page
</a>
</div>
</div>
</div>
);
}错误处理最佳实践
1. 错误边界层次设计
jsx
// 分层错误处理策略
const errorBoundaryStrategy = {
// 全局层 - 捕获致命错误
global: {
purpose: '处理应用级错误,防止整个应用崩溃',
features: ['错误日志', '用户反馈', '重启应用'],
example: '<RootErrorBoundary />'
},
// 布局层 - 保留导航
layout: {
purpose: '处理布局内容错误,保留应用框架',
features: ['保留导航', '错误恢复', '部分刷新'],
example: '<LayoutErrorBoundary />'
},
// 功能层 - 隔离功能模块
feature: {
purpose: '隔离单个功能模块的错误',
features: ['降级显示', '重试机制', '替代方案'],
example: '<FeatureErrorBoundary />'
},
// 组件层 - 最细粒度
component: {
purpose: '处理单个组件错误,不影响其他组件',
features: ['占位符', '默认值', '静默失败'],
example: '<ComponentErrorBoundary />'
}
};2. 错误信息本地化
jsx
// 多语言错误消息
const errorMessages = {
en: {
404: 'Page not found',
403: 'Access denied',
500: 'Server error',
network: 'Network error',
retry: 'Try again',
goBack: 'Go back',
goHome: 'Go to homepage'
},
zh: {
404: '页面未找到',
403: '访问被拒绝',
500: '服务器错误',
network: '网络错误',
retry: '重试',
goBack: '返回',
goHome: '返回首页'
}
};
function LocalizedErrorBoundary() {
const error = useRouteError();
const { language } = useLanguage();
const messages = errorMessages[language] || errorMessages.en;
return (
<div className="error-page">
<h1>{messages[error.status] || messages[500]}</h1>
<div className="error-actions">
<button onClick={() => navigate(0)}>
{messages.retry}
</button>
<button onClick={() => navigate(-1)}>
{messages.goBack}
</button>
<button onClick={() => navigate('/')}>
{messages.goHome}
</button>
</div>
</div>
);
}3. 性能监控
jsx
// 错误性能追踪
class ErrorPerformanceTracker {
constructor() {
this.metrics = [];
}
trackError(error, context = {}) {
const metric = {
timestamp: performance.now(),
error: {
message: error.message,
type: error.name
},
context,
performance: {
memory: performance.memory?.usedJSHeapSize,
navigation: performance.getEntriesByType('navigation')[0]
}
};
this.metrics.push(metric);
// 发送到分析服务
this.sendMetrics(metric);
}
async sendMetrics(metric) {
try {
await fetch('/api/metrics/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(metric)
});
} catch (error) {
console.error('Failed to send error metrics:', error);
}
}
getMetrics() {
return [...this.metrics];
}
}
const errorTracker = new ErrorPerformanceTracker();总结
React Router v6的错误处理机制提供了完善的解决方案:
- errorElement:路由级错误边界
- useRouteError:获取错误详情
- 嵌套处理:多层次错误隔离
- 错误恢复:重试、降级、部分失败处理
- 用户体验:友好的错误页面和交互
- 监控日志:完整的错误追踪和报告
合理的错误处理策略能够显著提升应用的健壮性和用户体验。