Vue3 Composition API完全指南
📋 概述
Vue3 Composition API提供了更灵活的逻辑复用和组织方式。通过ref、computed、watch等API,我们可以将ECharts的初始化、更新、销毁等操作封装成可复用的composable函数,实现更好的代码组织和类型安全。
核心价值
- 组合式API:更符合逻辑关注点分离
- 响应式集成:与Vue3响应式系统完美融合
- TypeScript支持:完整的类型推断
- 逻辑复用:composable函数可在多个组件间共享
- 生命周期管理:自动处理初始化和清理
🎯 核心概念
1. 基础Composable结构
typescript
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
import * as echarts from 'echarts';
export function useECharts(option: Ref<EChartsOption>) {
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
onMounted(() => {
if (chartRef.value) {
chartInstance = echarts.init(chartRef.value);
chartInstance.setOption(option.value);
}
});
onBeforeUnmount(() => {
if (chartInstance) {
chartInstance.dispose();
}
});
watch(option, (newOption) => {
if (chartInstance) {
chartInstance.setOption(newOption);
}
}, { deep: true });
return { 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
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. 使用方式
vue
<template>
<div ref="chartRef" style="width: 100%; height: 400px"></div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useECharts } from './useECharts';
const option = ref({
xAxis: { type: 'category', data: ['A', 'B', 'C'] },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: [10, 20, 30] }]
});
const { chartRef } = useECharts(option);
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
🔧 完整实现
TypeScript类型定义
typescript
// types.ts
import type { Ref } from 'vue';
import type { EChartsOption, EChartsType } from 'echarts';
export interface UseEChartsOptions {
theme?: string;
renderer?: 'canvas' | 'svg';
autoResize?: boolean;
loading?: Ref<boolean>;
onChartReady?: (chart: EChartsType) => void;
onChartError?: (error: Error) => void;
}
export interface UseEChartsReturn {
chartRef: Ref<HTMLElement | null>;
chartInstance: EChartsType | null;
resize: () => void;
updateOption: (option: EChartsOption) => void;
clear: () => void;
dispose: () => void;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
完整Composable实现
typescript
// useECharts.ts
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
import * as echarts from 'echarts';
import type { EChartsOption, EChartsType } from 'echarts';
import type { Ref } from 'vue';
import type { UseEChartsOptions, UseEChartsReturn } from './types';
export function useECharts(
option: Ref<EChartsOption>,
config: UseEChartsOptions = {}
): UseEChartsReturn {
const {
theme,
renderer = 'canvas',
autoResize = true,
loading,
onChartReady,
onChartError
} = config;
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: EChartsType | null = null;
let resizeObserver: ResizeObserver | null = null;
// 初始化图表
const initChart = async () => {
if (!chartRef.value) return;
await nextTick();
try {
// 创建实例
chartInstance = echarts.init(chartRef.value, theme, {
renderer
});
// 设置配置
chartInstance.setOption(option.value);
// 显示加载动画
if (loading?.value) {
chartInstance.showLoading();
}
// 调用就绪回调
onChartReady?.(chartInstance);
// 监听容器大小变化
if (autoResize && chartRef.value) {
resizeObserver = new ResizeObserver(() => {
chartInstance?.resize();
});
resizeObserver.observe(chartRef.value);
}
} catch (error) {
console.error('Failed to initialize ECharts:', error);
onChartError?.(error as Error);
}
};
// 更新配置
const updateChart = () => {
if (chartInstance) {
chartInstance.setOption(option.value, {
notMerge: false,
lazyUpdate: false
});
// 隐藏加载动画
if (!loading?.value) {
chartInstance.hideLoading();
}
}
};
// 生命周期钩子
onMounted(() => {
initChart();
});
onBeforeUnmount(() => {
// 清理资源
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
});
// 监听配置变化
watch(
option,
() => {
updateChart();
},
{ deep: true }
);
// 监听loading状态
if (loading) {
watch(loading, (isLoading) => {
if (chartInstance) {
if (isLoading) {
chartInstance.showLoading();
} else {
chartInstance.hideLoading();
}
}
});
}
// 手动方法
const resize = () => {
chartInstance?.resize();
};
const updateOption = (newOption: EChartsOption) => {
if (chartInstance) {
chartInstance.setOption(newOption);
}
};
const clear = () => {
chartInstance?.clear();
};
const dispose = () => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
};
return {
chartRef,
chartInstance,
resize,
updateOption,
clear,
dispose
};
}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
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
💡 实战案例
案例1:基础柱状图
vue
<template>
<div class="chart-container">
<h2>月度销售统计</h2>
<div ref="chartRef" class="chart"></div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useECharts } from './useECharts';
const option = ref({
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['一月', '二月', '三月', '四月', '五月']
},
yAxis: {
type: 'value'
},
series: [{
name: '销售额',
type: 'bar',
data: [120, 200, 150, 80, 70],
itemStyle: {
color: '#5470c6'
}
}]
});
const { chartRef } = useECharts(option);
</script>
<style scoped>
.chart-container {
padding: 20px;
}
.chart {
width: 100%;
height: 400px;
}
</style>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
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
案例2:动态数据更新
vue
<template>
<div>
<button @click="addData">添加数据</button>
<button @click="clearData">清空数据</button>
<div ref="chartRef" style="width: 100%; height: 400px"></div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useECharts } from './useECharts';
const dataList = ref<number[]>([10, 20, 30]);
const option = computed(() => ({
title: {
text: '动态数据',
left: 'center'
},
xAxis: {
type: 'category',
data: dataList.value.map((_, i) => `数据${i + 1}`)
},
yAxis: {
type: 'value'
},
series: [{
type: 'line',
data: dataList.value,
smooth: true
}]
}));
const { chartRef } = useECharts(option);
const addData = () => {
dataList.value.push(Math.random() * 100);
};
const clearData = () => {
dataList.value = [];
};
</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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
案例3:主题切换
vue
<template>
<div>
<el-radio-group v-model="theme">
<el-radio-button label="light">亮色</el-radio-button>
<el-radio-button label="dark">暗色</el-radio-button>
</el-radio-group>
<div ref="chartRef" style="width: 100%; height: 400px"></div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useECharts } from './useECharts';
const theme = ref('light');
const option = computed(() => ({
title: {
text: '主题切换示例',
left: 'center'
},
xAxis: {
type: 'category',
data: ['A', 'B', 'C', 'D']
},
yAxis: {
type: 'value'
},
series: [{
type: 'bar',
data: [10, 20, 30, 40]
}]
}));
const { chartRef } = useECharts(option, {
theme: theme as any,
autoResize: 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
29
30
31
32
33
34
35
36
37
38
39
40
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
案例4:事件监听
vue
<template>
<div>
<div ref="chartRef" style="width: 100%; height: 400px"></div>
<div v-if="clickedData">
点击了: {{ clickedData.name }} - {{ clickedData.value }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useECharts } from './useECharts';
import type { EChartsType } from 'echarts';
const clickedData = ref<any>(null);
const option = ref({
xAxis: {
type: 'category',
data: ['A', 'B', 'C', 'D']
},
yAxis: {
type: 'value'
},
series: [{
type: 'bar',
data: [10, 20, 30, 40]
}]
});
const { chartRef } = useECharts(option, {
onChartReady: (chart: EChartsType) => {
// 监听点击事件
chart.on('click', (params) => {
clickedData.value = {
name: params.name,
value: params.value
};
console.log('点击了:', params);
});
}
});
</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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
⚠️ 常见问题
问题1:图表不显示
解决:
vue
<!-- ❌ 错误:没有设置宽高 -->
<div ref="chartRef"></div>
<!-- ✅ 正确:设置明确的宽高 -->
<div ref="chartRef" style="width: 100%; height: 400px"></div>1
2
3
4
5
2
3
4
5
问题2:响应式失效
解决:
typescript
// ❌ 错误:使用普通对象
const option = { ... };
// ✅ 正确:使用ref或computed
const option = ref({ ... });
// 或
const option = computed(() => ({ ... }));1
2
3
4
5
6
7
2
3
4
5
6
7
问题3:内存泄漏
解决:
typescript
// useECharts中已处理
onBeforeUnmount(() => {
if (chartInstance) {
chartInstance.dispose();
}
if (resizeObserver) {
resizeObserver.disconnect();
}
});1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
🎯 最佳实践
1. 性能优化
typescript
// 使用shallowRef减少响应式开销
import { shallowRef } from 'vue';
const option = shallowRef<EChartsOption>({
// 大型配置对象
});1
2
3
4
5
6
2
3
4
5
6
2. 逻辑复用
typescript
// composables/useBarChart.ts
export function useBarChart(data: Ref<number[]>) {
const option = computed(() => ({
xAxis: { type: 'category', data: ['A', 'B', 'C'] },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: data.value }]
}));
return useECharts(option);
}
// 组件中使用
const data = ref([10, 20, 30]);
const { chartRef } = useBarChart(data);1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
3. 错误处理
typescript
const { chartRef } = useECharts(option, {
onChartError: (error) => {
console.error('图表错误:', error);
// 上报错误监控
}
});1
2
3
4
5
6
2
3
4
5
6
📊 性能指标
| 操作 | 耗时 | 说明 |
|---|---|---|
| 初始化 | 50-100ms | 首次创建 |
| 响应式更新 | 15-30ms | watch触发 |
| 调整大小 | 5-15ms | resize |
| 销毁 | 5-10ms | dispose |
🔗 相关链接
💎 总结
Vue3 Composable核心价值:
- ✅ 组合式API,逻辑清晰
- ✅ 响应式系统集成
- ✅ TypeScript类型安全
- ✅ 易于测试和复用
关键使用原则:
- 使用ref/computed:确保响应式
- 及时清理资源:避免内存泄漏
- 使用ResizeObserver:精确监听容器变化
- 提取composable:复用图表逻辑
掌握Vue3 Composition API,让ECharts在Vue中更优雅!💚
