Web Component封装完全指南
📋 概述
Web Component是浏览器原生的组件化标准,可以在任何框架中使用。通过将ECharts封装为Web Component,可以实现真正的框架无关、可复用的图表组件。
核心价值
- 框架无关:在React、Vue、Angular或原生JS中使用
- 原生支持:基于浏览器标准,无需额外依赖
- 样式隔离:Shadow DOM提供天然的作用域隔离
- 生命周期:内置connectedCallback、disconnectedCallback
- 易于分发:可作为独立包发布
🎯 核心概念
1. 基础Web Component
typescript
class EChartsElement extends HTMLElement {
private chart: echarts.ECharts | null = null;
static get observedAttributes() {
return ['option', 'theme', 'renderer'];
}
connectedCallback() {
this.initChart();
}
disconnectedCallback() {
this.disposeChart();
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
if (name === 'option' && this.chart) {
this.chart.setOption(JSON.parse(newValue));
}
}
private initChart() {
const option = JSON.parse(this.getAttribute('option') || '{}');
const theme = this.getAttribute('theme') || undefined;
this.chart = echarts.init(this, theme);
this.chart.setOption(option);
}
private disposeChart() {
if (this.chart) {
this.chart.dispose();
this.chart = null;
}
}
}
customElements.define('e-charts', EChartsElement);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
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
2. 使用方式
html
<!-- HTML -->
<e-charts
option='{"xAxis":{"type":"category","data":["A","B"]},"yAxis":{"type":"value"},"series":[{"type":"bar","data":[10,20]}]}'
style="width: 600px; height: 400px;">
</e-charts>
<!-- JavaScript -->
<script type="module" src="./e-charts-element.js"></script>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
🔧 完整实现
typescript
// e-charts-element.ts
import * as echarts from 'echarts';
class EChartsElement extends HTMLElement {
private chart: echarts.ECharts | null = null;
private resizeObserver: ResizeObserver | null = null;
// 监听的属性
static get observedAttributes() {
return ['option', 'theme', 'renderer', 'loading'];
}
// 连接到DOM
connectedCallback() {
this.initChart();
this.setupResizeObserver();
}
// 从DOM移除
disconnectedCallback() {
this.disposeChart();
this.disconnectResizeObserver();
}
// 属性变化
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
if (!this.chart || oldValue === newValue) return;
switch (name) {
case 'option':
this.updateChart();
break;
case 'loading':
this.updateLoading();
break;
}
}
// 初始化图表
private initChart() {
try {
const option = this.getOption();
const theme = this.getAttribute('theme') || undefined;
const renderer = (this.getAttribute('renderer') as 'canvas' | 'svg') || 'canvas';
this.chart = echarts.init(this, theme, { renderer });
this.chart.setOption(option);
// 初始loading状态
this.updateLoading();
// 触发自定义事件
this.dispatchEvent(new CustomEvent('chart-ready', {
detail: { chart: this.chart }
}));
} catch (error) {
console.error('Failed to initialize ECharts:', error);
this.dispatchEvent(new CustomEvent('chart-error', {
detail: { error }
}));
}
}
// 更新图表
private updateChart() {
if (!this.chart) return;
const option = this.getOption();
this.chart.setOption(option, { notMerge: false });
}
// 更新loading状态
private updateLoading() {
if (!this.chart) return;
const loading = this.getAttribute('loading') === 'true';
if (loading) {
this.chart.showLoading();
} else {
this.chart.hideLoading();
}
}
// 销毁图表
private disposeChart() {
if (this.chart) {
this.chart.dispose();
this.chart = null;
}
}
// 设置ResizeObserver
private setupResizeObserver() {
if ('ResizeObserver' in window) {
this.resizeObserver = new ResizeObserver(() => {
this.chart?.resize();
});
this.resizeObserver.observe(this);
} else {
// 降级方案:监听窗口resize
window.addEventListener('resize', this.handleResize);
}
}
// 断开ResizeObserver
private disconnectResizeObserver() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
} else {
window.removeEventListener('resize', this.handleResize);
}
}
// 处理resize
private handleResize = () => {
this.chart?.resize();
};
// 获取配置
private getOption() {
const optionStr = this.getAttribute('option') || '{}';
try {
return JSON.parse(optionStr);
} catch (error) {
console.error('Invalid option JSON:', error);
return {};
}
}
// 公共方法
public getInstance() {
return this.chart;
}
public resize() {
this.chart?.resize();
}
public clear() {
this.chart?.clear();
}
public dispose() {
this.disposeChart();
}
}
// 注册自定义元素
customElements.define('e-charts', EChartsElement);
export default EChartsElement;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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
💡 实战案例
案例1:基础使用
html
<!DOCTYPE html>
<html>
<head>
<title>ECharts Web Component</title>
<script type="module" src="./e-charts-element.js"></script>
</head>
<body>
<e-charts
id="myChart"
option='{
"title": {"text": "销售统计", "left": "center"},
"xAxis": {"type": "category", "data": ["一月", "二月", "三月"]},
"yAxis": {"type": "value"},
"series": [{"type": "bar", "data": [120, 200, 150]}]
}'
style="width: 600px; height: 400px;">
</e-charts>
<script>
const chart = document.getElementById('myChart');
// 监听就绪事件
chart.addEventListener('chart-ready', (e) => {
console.log('图表已就绪', e.detail.chart);
});
// 调用实例方法
setTimeout(() => {
chart.getInstance().dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: 0
});
}, 1000);
</script>
</body>
</html>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
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
案例2:在React中使用
tsx
import React, { useEffect, useRef } from 'react';
import './e-charts-element'; // 导入Web Component
interface EChartsProps {
option: any;
theme?: string;
style?: React.CSSProperties;
}
function ECharts({ option, theme, style }: EChartsProps) {
const chartRef = useRef<HTMLElement>(null);
useEffect(() => {
if (chartRef.current) {
(chartRef.current as any).setAttribute('option', JSON.stringify(option));
}
}, [option]);
return (
<e-charts
ref={chartRef}
theme={theme}
style={style}
/>
);
}
export default ECharts;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
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
案例3:在Vue中使用
vue
<template>
<e-charts
ref="chartRef"
:option="chartOption"
:theme="theme"
style="width: 100%; height: 400px;"
/>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import './e-charts-element';
const chartRef = ref<HTMLElement | null>(null);
const theme = ref('light');
const chartOption = ref({
xAxis: { type: 'category', data: ['A', 'B', 'C'] },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: [10, 20, 30] }]
});
watch(chartOption, (newOption) => {
if (chartRef.value) {
(chartRef.value as any).setAttribute('option', JSON.stringify(newOption));
}
}, { deep: true });
</script>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
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
⚠️ 常见问题
问题1:属性更新不生效
解决:
typescript
// 确保在attributeChangedCallback中处理
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
if (name === 'option' && this.chart) {
this.updateChart();
}
}1
2
3
4
5
6
2
3
4
5
6
问题2:样式隔离
解决:
typescript
// 使用Shadow DOM(可选)
class EChartsElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
🎯 最佳实践
1. 错误处理
typescript
try {
this.chart = echarts.init(this, theme);
} catch (error) {
this.dispatchEvent(new CustomEvent('chart-error', {
detail: { error }
}));
}1
2
3
4
5
6
7
2
3
4
5
6
7
2. 性能优化
typescript
// 使用防抖
private handleResize = debounce(() => {
this.chart?.resize();
}, 100);1
2
3
4
2
3
4
📊 性能指标
| 操作 | 耗时 | 说明 |
|---|---|---|
| 初始化 | 60-120ms | 包含元素创建 |
| 属性更新 | 15-30ms | setOption |
| 调整大小 | 5-15ms | resize |
🔗 相关链接
💎 总结
Web Component核心价值:
- ✅ 框架无关,通用性强
- ✅ 原生支持,无额外依赖
- ✅ 样式隔离,作用域清晰
- ✅ 生命周期完善
掌握Web Component,让图表组件真正通用!🌐
