ECharts 错误处理机制
📋 概述
在生产环境中,ECharts应用可能面临各种错误场景:数据异常、渲染失败、内存溢出等。完善的错误处理机制能够:
- ✅ 优雅降级:在错误发生时保持应用可用
- ✅ 快速定位:提供详细的错误信息和堆栈
- ✅ 自动恢复:尝试从错误中恢复
- ✅ 用户体验:显示友好的错误提示
本文档详细介绍ECharts的错误处理策略和最佳实践。
🎯 核心概念
错误分类
typescript
// 错误类型定义
enum ChartErrorType {
/** 数据错误 */
DATA_ERROR = 'DATA_ERROR',
/** 配置错误 */
CONFIG_ERROR = 'CONFIG_ERROR',
/** 渲染错误 */
RENDER_ERROR = 'RENDER_ERROR',
/** 性能错误 */
PERFORMANCE_ERROR = 'PERFORMANCE_ERROR',
/** 资源错误 */
RESOURCE_ERROR = 'RESOURCE_ERROR'
}
interface ChartError {
type: ChartErrorType;
message: string;
details?: any;
stack?: string;
timestamp: number;
chartId?: string;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
错误级别
typescript
enum ErrorLevel {
/** 信息 - 不影响功能 */
INFO = 'INFO',
/** 警告 - 可能影响部分功能 */
WARNING = 'WARNING',
/** 错误 - 功能不可用但可恢复 */
ERROR = 'ERROR',
/** 致命 - 系统崩溃 */
FATAL = 'FATAL'
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
🔧 错误处理实现
1. 全局错误边界
React Error Boundary
typescript
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode | ((error: Error) => ReactNode);
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ChartErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// 记录错误日志
console.error('Chart Error Boundary caught:', error, errorInfo);
// 调用自定义错误处理
this.props.onError?.(error, errorInfo);
// 上报到监控系统
reportError({
name: error.name,
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack
});
}
render() {
if (this.state.hasError) {
// 自定义fallback UI
if (this.props.fallback) {
return typeof this.props.fallback === 'function'
? this.props.fallback(this.state.error!)
: this.props.fallback;
}
// 默认错误UI
return (
<div className="chart-error">
<div className="error-icon">⚠️</div>
<h3>图表加载失败</h3>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>
重试
</button>
</div>
);
}
return this.props.children;
}
}
// 使用示例
function App() {
return (
<ChartErrorBoundary
fallback={<div>图表暂时不可用</div>}
onError={(error, errorInfo) => {
console.log('Error details:', error, errorInfo);
}}
>
<MyChart />
</ChartErrorBoundary>
);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
Vue Error Handler
typescript
import { createApp, App as VueApp } from 'vue';
export function setupGlobalErrorHandler(app: VueApp) {
app.config.errorHandler = (err, instance, info) => {
console.error('Vue Error Handler:', err, info);
// 上报错误
reportError({
name: err instanceof Error ? err.name : 'Unknown',
message: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
component: instance?.$options?.name || 'Unknown',
info
});
};
}
// 组件级错误处理
export function withErrorHandling<T extends object>(Component: T): T {
return {
...Component,
errorCaptured(err: Error, instance: any, info: string) {
console.error('Error captured:', err, info);
// 可以返回false阻止错误向上传播
return false;
}
} as any;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2. 数据验证与错误预防
数据类型检查
typescript
/**
* 验证图表数据的合法性
*/
export function validateChartData(data: any): { valid: boolean; errors: string[] } {
const errors: string[] = [];
// 检查数组类型
if (!Array.isArray(data)) {
errors.push('Data must be an array');
return { valid: false, errors };
}
// 检查空数组
if (data.length === 0) {
errors.push('Data array is empty');
}
// 检查数值合法性
data.forEach((value, index) => {
if (typeof value === 'number') {
if (isNaN(value)) {
errors.push(`Data at index ${index} is NaN`);
} else if (!isFinite(value)) {
errors.push(`Data at index ${index} is not finite`);
} else if (Math.abs(value) > Number.MAX_SAFE_INTEGER) {
errors.push(`Data at index ${index} exceeds safe integer range`);
}
}
});
return {
valid: errors.length === 0,
errors
};
}
/**
* 安全的数据转换
*/
export function safeDataTransform(rawData: any[]): number[] {
return rawData.map((value, index) => {
// 转换为数字
const num = Number(value);
// 检查转换结果
if (isNaN(num)) {
console.warn(`Invalid data at index ${index}: ${value}, using 0 instead`);
return 0;
}
if (!isFinite(num)) {
console.warn(`Infinite data at index ${index}: ${value}, using 0 instead`);
return 0;
}
return num;
});
}
// 使用示例
const rawData = [10, 20, 'invalid', NaN, Infinity, 50];
const safeData = safeDataTransform(rawData);
// [10, 20, 0, 0, 0, 50]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
配置项验证
typescript
interface ValidationResult {
isValid: boolean;
warnings: string[];
}
export function validateChartOption(option: EChartsOption): ValidationResult {
const warnings: string[] = [];
// 检查series是否存在
if (!option.series || option.series.length === 0) {
warnings.push('No series defined in chart option');
}
// 检查坐标轴配置
if (option.xAxis && Array.isArray(option.xAxis)) {
option.xAxis.forEach((axis, index) => {
if (axis.type === 'category' && !axis.data) {
warnings.push(`XAxis[${index}] is category type but has no data`);
}
});
}
// 检查数据范围
if (option.series) {
option.series.forEach((series, index) => {
if ('data' in series && series.data) {
const validation = validateChartData(series.data as any[]);
if (!validation.valid) {
warnings.push(`Series[${index}] has invalid data: ${validation.errors.join(', ')}`);
}
}
});
}
return {
isValid: warnings.length === 0,
warnings
};
}
// 使用示例
const result = validateChartOption(chartOption);
if (!result.isValid) {
console.error('Invalid chart option:', result.warnings);
} else if (result.warnings.length > 0) {
console.warn('Chart warnings:', result.warnings);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
3. 渲染错误处理
安全的图表初始化
typescript
class SafeChartRenderer {
private chart: echarts.ECharts | null = null;
private retryCount = 0;
private maxRetries = 3;
/**
* 安全地初始化图表
*/
async safeInit(container: HTMLElement, option: EChartsOption): Promise<void> {
try {
// 前置验证
if (!container) {
throw new Error('Container element is null');
}
if (container.clientWidth === 0 || container.clientHeight === 0) {
console.warn('Container has zero size, waiting for layout...');
await this.waitForSize(container);
}
// 验证配置
const validation = validateChartOption(option);
if (!validation.isValid) {
throw new Error(`Invalid chart option: ${validation.warnings.join(', ')}`);
}
// 创建实例
this.chart = echarts.init(container, undefined, {
renderer: 'canvas'
});
// 设置配置
this.chart.setOption(option, { notMerge: true });
// 重置重试计数
this.retryCount = 0;
} catch (error) {
console.error('Chart initialization failed:', error);
await this.handleInitError(container, option, error);
}
}
/**
* 处理初始化错误
*/
private async handleInitError(
container: HTMLElement,
option: EChartsOption,
error: any
): Promise<void> {
if (this.retryCount < this.maxRetries) {
this.retryCount++;
console.log(`Retrying initialization (${this.retryCount}/${this.maxRetries})...`);
// 延迟重试
await this.delay(1000 * this.retryCount);
await this.safeInit(container, option);
} else {
// 达到最大重试次数,显示错误状态
this.showErrorState(container, error);
throw error;
}
}
/**
* 等待容器有有效尺寸
*/
private waitForSize(container: HTMLElement): Promise<void> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
observer.disconnect();
reject(new Error('Timeout waiting for container size'));
}, 5000);
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
clearTimeout(timeout);
observer.disconnect();
resolve();
}
});
observer.observe(container);
});
}
/**
* 显示错误状态
*/
private showErrorState(container: HTMLElement, error: any): void {
container.innerHTML = `
<div style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
padding: 20px;
">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
<h3 style="margin-top: 16px;">图表加载失败</h3>
<p style="font-size: 12px; margin-top: 8px;">${error.message}</p>
</div>
`;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
4. 运行时错误监控
性能异常检测
typescript
class RuntimeMonitor {
private readonly MAX_RENDER_TIME = 5000; // 5秒
private readonly MAX_MEMORY = 500 * 1024 * 1024; // 500MB
/**
* 监控渲染时间
*/
monitorRenderTime(option: EChartsOption): void {
const startTime = performance.now();
// Hook setOption
const originalSetOption = echarts.ECharts.prototype.setOption;
echarts.ECharts.prototype.setOption = function (...args) {
const result = originalSetOption.apply(this, args);
const renderTime = performance.now() - startTime;
if (renderTime > this.MAX_RENDER_TIME) {
console.warn(`Chart render time exceeded: ${renderTime.toFixed(2)}ms`);
// 上报性能问题
reportPerformanceIssue({
type: 'SLOW_RENDER',
duration: renderTime,
threshold: this.MAX_RENDER_TIME,
option: JSON.stringify(option)
});
}
return result;
};
}
/**
* 监控内存使用
*/
monitorMemory(): void {
if (performance.memory) {
const memoryUsage = performance.memory.usedJSHeapSize;
if (memoryUsage > this.MAX_MEMORY) {
console.warn(`High memory usage detected: ${(memoryUsage / 1024 / 1024).toFixed(2)}MB`);
// 建议清理
this.suggestCleanup();
}
}
}
/**
* 建议清理策略
*/
private suggestCleanup(): void {
console.log('Suggested cleanup actions:');
console.log('1. Dispose unused chart instances');
console.log('2. Clear large datasets');
console.log('3. Remove off-screen charts');
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
5. 错误恢复策略
自动降级方案
typescript
/**
* 图表降级渲染器
*/
class FallbackRenderer {
/**
* 根据错误类型选择降级方案
*/
static async renderFallback(
container: HTMLElement,
errorType: ChartErrorType,
originalOption?: EChartsOption
): Promise<void> {
switch (errorType) {
case ChartErrorType.PERFORMANCE_ERROR:
// 性能问题:降低渲染质量
this.renderLowQuality(container, originalOption);
break;
case ChartErrorType.DATA_ERROR:
// 数据错误:显示空状态
this.renderEmptyState(container);
break;
case ChartErrorType.RENDER_ERROR:
// 渲染错误:尝试SVG模式
this.renderWithSVG(container, originalOption);
break;
default:
// 通用错误:显示错误消息
this.renderErrorMessage(container, errorType);
}
}
/**
* 低质量渲染(减少动画、简化图形)
*/
private static renderLowQuality(container: HTMLElement, option?: EChartsOption): void {
try {
const chart = echarts.init(container, undefined, {
renderer: 'canvas'
});
// 禁用动画
const lowQualityOption = {
...option,
animation: false,
progressive: 0,
hoverAnimation: false
};
chart.setOption(lowQualityOption);
console.info('Chart rendered in low quality mode');
} catch (error) {
console.error('Low quality render failed:', error);
this.renderErrorMessage(container, ChartErrorType.RENDER_ERROR);
}
}
/**
* 空状态
*/
private static renderEmptyState(container: HTMLElement): void {
container.innerHTML = `
<div style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #ccc;
">
<svg width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<line x1="3" y1="9" x2="21" y2="9"/>
<line x1="9" y1="21" x2="9" y2="9"/>
</svg>
<p style="margin-top: 16px;">暂无数据</p>
</div>
`;
}
/**
* SVG模式渲染
*/
private static renderWithSVG(container: HTMLElement, option?: EChartsOption): void {
try {
const chart = echarts.init(container, undefined, {
renderer: 'svg' // 切换到SVG渲染器
});
if (option) {
chart.setOption(option);
}
console.info('Chart rendered with SVG renderer');
} catch (error) {
console.error('SVG render failed:', error);
this.renderErrorMessage(container, ChartErrorType.RENDER_ERROR);
}
}
/**
* 显示错误消息
*/
private static renderErrorMessage(container: HTMLElement, errorType: ChartErrorType): void {
container.innerHTML = `
<div style="padding: 20px; text-align: center; color: #f56c6c;">
<h4>图表加载失败</h4>
<p>错误类型: ${errorType}</p>
<button onclick="location.reload()">刷新页面</button>
</div>
`;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
💡 实战案例
案例1:完整的错误处理Hook
typescript
import { useState, useCallback } from 'react';
interface UseErrorHandlingReturn {
error: Error | null;
isError: boolean;
handleError: (error: Error, context?: string) => void;
clearError: () => void;
retry: (fn: () => Promise<void>) => Promise<void>;
}
export function useErrorHandling(maxRetries = 3): UseErrorHandlingReturn {
const [error, setError] = useState<Error | null>(null);
const [retryCount, setRetryCount] = useState(0);
const handleError = useCallback((err: Error, context?: string) => {
const errorMessage = context
? `${context}: ${err.message}`
: err.message;
console.error('Error handled:', errorMessage, err.stack);
// 上报错误
reportToSentry({
error: err,
context,
retryCount
});
setError(err);
}, [retryCount]);
const clearError = useCallback(() => {
setError(null);
setRetryCount(0);
}, []);
const retry = useCallback(async (fn: () => Promise<void>) => {
try {
await fn();
clearError();
} catch (err) {
if (retryCount < maxRetries) {
setRetryCount(prev => prev + 1);
console.log(`Retrying... (${retryCount + 1}/${maxRetries})`);
// 指数退避
await new Promise(resolve => setTimeout(resolve, Math.pow(2, retryCount) * 1000));
await retry(fn);
} else {
handleError(err instanceof Error ? err : new Error(String(err)));
}
}
}, [retryCount, maxRetries, clearError, handleError]);
return {
error,
isError: error !== null,
handleError,
clearError,
retry
};
}
// 使用示例
function MyChart() {
const { error, isError, handleError, retry } = useErrorHandling();
const loadChart = useCallback(async () => {
await retry(async () => {
const data = await fetchData();
initChart(data);
});
}, [retry]);
if (isError) {
return (
<div>
<p>加载失败: {error?.message}</p>
<button onClick={loadChart}>重试</button>
</div>
);
}
return <div ref={chartRef} />;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
案例2:错误日志上报
typescript
interface ErrorReport {
name: string;
message: string;
stack?: string;
context?: Record<string, any>;
timestamp: number;
userId?: string;
url: string;
}
class ErrorReporter {
private queue: ErrorReport[] = [];
private readonly MAX_QUEUE_SIZE = 50;
private isReporting = false;
/**
* 上报错误
*/
report(error: Partial<ErrorReport> & Pick<ErrorReport, 'message'>): void {
const report: ErrorReport = {
name: error.name || 'Error',
message: error.message,
stack: error.stack,
context: error.context,
timestamp: Date.now(),
userId: this.getUserId(),
url: window.location.href,
...error
};
// 添加到队列
this.queue.push(report);
// 立即发送或批量发送
if (this.queue.length >= this.MAX_QUEUE_SIZE) {
this.flushQueue();
} else if (!this.isReporting) {
// 延迟发送,收集更多错误
setTimeout(() => this.flushQueue(), 5000);
}
}
/**
* 发送错误队列
*/
private async flushQueue(): Promise<void> {
if (this.isReporting || this.queue.length === 0) {
return;
}
this.isReporting = true;
const reports = [...this.queue];
this.queue = [];
try {
await navigator.sendBeacon('/api/errors', JSON.stringify(reports));
console.log(`Sent ${reports.length} error reports`);
} catch (error) {
console.error('Failed to send error reports:', error);
// 重新加入队列
this.queue.unshift(...reports);
} finally {
this.isReporting = false;
}
}
private getUserId(): string | undefined {
// 从localStorage或cookie获取用户ID
return localStorage.getItem('userId') || undefined;
}
}
export const errorReporter = new ErrorReporter();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
⚠️ 常见问题
问题1:错误被静默吞掉
症状:图表不显示但没有错误信息
原因:try-catch捕获后没有记录或上报
解决:
typescript
// ❌ 错误做法
try {
chart.setOption(option);
} catch (e) {
// 什么都不做
}
// ✅ 正确做法
try {
chart.setOption(option);
} catch (error) {
console.error('Chart setOption failed:', error);
errorReporter.report({
message: error instanceof Error ? error.message : 'Unknown error',
context: { option }
});
throw error; // 或者显示错误UI
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
问题2:重复错误上报
症状:同一个错误被多次上报
解决:错误去重
typescript
class DedupErrorReporter extends ErrorReporter {
private readonly REPORTED_ERRORS = new Set<string>();
private readonly DEDUP_WINDOW = 60000; // 1分钟
report(error: ErrorReport): void {
const key = this.getErrorKey(error);
if (this.REPORTED_ERRORS.has(key)) {
console.log('Duplicate error skipped:', key);
return;
}
super.report(error);
this.REPORTED_ERRORS.add(key);
// 清理过期记录
setTimeout(() => this.REPORTED_ERRORS.delete(key), this.DEDUP_WINDOW);
}
private getErrorKey(error: ErrorReport): string {
return `${error.name}:${error.message}:${error.context?.chartId}`;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
🎯 最佳实践
1. 多层错误处理
┌─────────────────────────────────────┐
│ 应用层: Error Boundary │
│ ├─ 显示友好错误UI │
│ └─ 提供重试按钮 │
├─────────────────────────────────────┤
│ 业务层: 数据验证 │
│ ├─ 检查数据合法性 │
│ └─ 提供默认值 │
├─────────────────────────────────────┤
│ 框架层: 全局错误处理器 │
│ ├─ 捕获未处理错误 │
│ └─ 上报监控系统 │
└─────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
2. 错误分级处理
| 级别 | 处理方式 | 是否上报 | 用户可见 |
|---|---|---|---|
| INFO | 控制台日志 | 否 | 否 |
| WARNING | 控制台警告 | 可选 | 否 |
| ERROR | 错误UI + 重试 | 是 | 是 |
| FATAL | 全局错误页 | 是 | 是 |
3. 优雅的降级顺序
正常渲染 → 低质量渲染 → SVG模式 → 静态图片 → 空状态 → 错误消息1
📊 错误处理指标
| 指标 | 目标 | 说明 |
|---|---|---|
| 错误捕获率 | > 95% | 未被捕获的异常应少于5% |
| 错误上报延迟 | < 5秒 | 从发生到上报的时间 |
| 重试成功率 | 30-50% | 重试后成功的比例 |
| 降级可用性 | > 99% | 降级后仍可使用 |
🔗 相关链接
💎 总结
完善的错误处理机制是生产级ECharts应用的必备条件。通过多层错误边界、数据验证、自动降级和错误上报,可以在保证用户体验的同时快速定位和解决问题。记住:不要相信任何输入,永远为失败做好准备。
