前言
在前端开发中,ECharts 作为数据可视化的利器被广泛使用,但每次使用都要重复处理初始化、容器获取、事件绑定、窗口 resize 等逻辑,不仅繁琐还容易出错。最近我封装了一个useEchart Hooks,彻底解决了这些痛点,今天就来分享一下实现思路和使用技巧。
为什么需要这个 Hooks?
先看看我们平时用 ECharts 的常规操作:
ts
// 常规写法
let chart = null;
// 初始化
onMounted(() => {
const dom = document.getElementById('chart-container');
if (dom) {
chart = echarts.init(dom);
chart.setOption(option);
window.addEventListener('resize', handleResize);
}
});
// 更新数据
const updateChart = (newData) => {
if (chart) {
chart.setOption({ series: [{ data: newData }] });
}
};
// 处理resize
const handleResize = () => {
chart?.resize();
};
// 销毁实例
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
chart?.dispose();
});
这段代码不算复杂,但每个图表都要写一遍就很折磨人了。更麻烦的是:
- 容器获取要处理各种情况(DOM 元素、ID 选择器、Vue Ref)
- 频繁初始化容易导致内存泄漏
- 事件绑定 / 解绑需要手动管理
- 响应式数据更新要手动触发 setOption
useEchart Hooks 来了!
基于以上痛点,我封装了useEchart Hooks,核心功能包括:
- 支持多种容器类型(Ref、DOM 元素、ID / 类选择器)
- 自动处理初始化与销毁
- 响应式配置更新
- 内置事件绑定 / 解绑方法
- 自动监听窗口 resize
废话不多说先上代码!
ts
//先导入Echart
import { echarts } from "@/Echarts";
export interface RefObject {
current?: HTMLElement | null;
}
export interface CallbackRef {
(el: HTMLElement | null): void;
}
export type EchartsOption = echarts.EChartsOption;
export type container =
| Ref<HTMLElement | null>
| HTMLElement
| string
| string[];
/**
* 适配多种容器选择方式的 ECharts 封装
* @param container - 容器选择器(支持 Ref, DOM 元素, ID 选择器, 类选择器)
* @param option - 初始配置
* @returns {chart: echarts.ECharts | null, update: (newOption: EChartsOption) => void}
*/
export function useEchart(
container: container,
option: EchartsOption = {}
): {
chart: echarts.ECharts | null;
onChartEvent: (event: string, handler: (params: any) => void) => void;
offChartEvent: (event: string, handler: (params: any) => void) => void;
update: (newOption: EchartsOption) => void;
handleResize: () => void;
} {
let chart: echarts.ECharts | null = null;
let containerElement: HTMLElement | null = null;
// 辅助函数处理单个选择器
const getContainerElementForSingle = (
selector: string
): HTMLElement | null => {
if (selector.startsWith("#")) {
return document.getElementById(selector.slice(1)) || null;
} else if (selector.startsWith(".")) {
return (document.querySelector(selector) as HTMLElement) || null;
}
// 直接ID 无#
return document.getElementById(selector) || null;
};
//获取容器元素
const getContainerElement = (): HTMLElement | null => {
if (container instanceof HTMLElement) {
return container;
} else if (typeof container === "string") {
return getContainerElementForSingle(container);
} else if ("value" in container) {
// Ref 类型
return container.value;
} else if (Array.isArray(container)) {
// 多个选择器(返回第一个匹配)
for (const selector of container) {
const element = getContainerElementForSingle(selector);
if (element) {
return element;
}
}
}
return null;
};
// 初始化图表
const initChart = (): void => {
containerElement = getContainerElement();
if (!containerElement) {
console.error("无法获取容器元素");
return;
}
if (!chart) {
chart = echarts.init(containerElement, "infographic");
// if (containerElement) {
// containerElement.removeAttribute("_echarts_instance_");
// }
chart.resize();
window.addEventListener("resize", handleResize);
}
if (option) {
chart.setOption(option);
}
};
// 处理窗口大小变化
const handleResize = () => {
chart?.resize();
};
// 更新图表配置
const update = (newOption: EchartsOption): void => {
if (chart) {
chart.setOption(newOption);
}
};
// 新增:事件绑定方法
const onChartEvent = (event: string, handler: (params: any) => void) => {
chart?.on(event, handler);
};
const offChartEvent = (event: string, handler: (params: any) => void) => {
chart?.off(event, handler);
};
// 响应式更新图表配置
watch(
() => option,
(newOption) => update(newOption),
{
deep: true,
}
);
onMounted(() => {
initChart();
});
onBeforeUnmount(() => {
if (chart) {
window.removeEventListener("resize", handleResize);
chart.dispose();
chart = null;
}
});
return {
get chart() {
return chart;
},
onChartEvent,
offChartEvent,
update,
handleResize
};
}
核心代码解析
先看整体结构,这个 Hooks 主要包含这些部分:
ts
export function useEchart(container, option) {
let chart = null;
let containerElement = null;
// 容器获取逻辑
const getContainerElement = () => { ... };
// 初始化图表
const initChart = () => { ... };
// 响应式更新
watch(() => option, (newOption) => { ... });
// 生命周期管理
onMounted(() => initChart());
onBeforeUnmount(() => { ... });
// 暴露API
return { chart, update, onChartEvent, offChartEvent, handleResize };
}
1. 万能容器处理
最实用的功能之一就是支持多种容器形式:
ts
// 支持的容器类型
type container = Ref<HTMLElement | null> | HTMLElement | string | string[];
// 容器获取逻辑
const getContainerElement = () => {
if (container instanceof HTMLElement) {
return container;
} else if (typeof container === "string") {
return getContainerElementForSingle(container);
} else if ("value" in container) { // Vue Ref
return container.value;
} else if (Array.isArray(container)) { // 多个选择器
for (const selector of container) {
const element = getContainerElementForSingle(selector);
if (element) return element;
}
}
return null;
};
无论是直接传 DOM 元素、Vue 的 Ref 对象,还是 ID 选择器(带 #或不带)、类选择器,甚至是选择器数组(自动取第一个匹配项),都能轻松处理。
2. 自动生命周期管理
初始化逻辑会在组件挂载时执行,销毁时自动清理:
ts
// 初始化图表
const initChart = () => {
containerElement = getContainerElement();
if (!containerElement) {
console.error("无法获取容器元素");
return;
}
if (!chart) {
chart = echarts.init(containerElement, "infographic");
chart.resize();
window.addEventListener("resize", handleResize);
}
chart.setOption(option);
};
// 组件卸载时清理
onBeforeUnmount(() => {
if (chart) {
window.removeEventListener("resize", handleResize);
chart.dispose();
chart = null;
}
});
再也不用担心忘记解绑事件或销毁实例导致的内存泄漏了!
3. 响应式与事件处理
内置 watch 监听配置变化,自动更新图表:
ts
// 响应式更新图表配置
watch(
() => option,
(newOption) => update(newOption),
{ deep: true }
);
// 事件绑定方法
const onChartEvent = (event: string, handler: (params: any) => void) => {
chart?.on(event, handler);
};
const offChartEvent = (event: string, handler: (params: any) => void) => {
chart?.off(event, handler);
};
如何使用?
用起来超级简单,三步到位:
1. 基础使用
ts
<template>
<div ref="chartRef" class="chart-container"></div>
</template>
<script setup>
import { ref } from 'vue';
import { useEchart } from './useEchart';
// 图表容器
const chartRef = ref(null);
// 初始配置
const option = ref({
xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
yAxis: { type: 'value' },
series: [{ data: [120, 200, 150], type: 'line' }]
});
// 初始化图表
const { chart, update } = useEchart(chartRef, option.value);
</script>
2. 事件绑定
ts
// 绑定点击事件
const { onChartEvent } = useEchart(chartRef, option.value);
onChartEvent('click', (params) => {
console.log('点击了图表', params);
});
3. 动态更新数据
ts
// 直接更新配置
const { update } = useEchart(chartRef, option.value);
// 按钮点击更新数据
const handleUpdate = () => {
update({
series: [{ data: [300, 150, 280], type: 'line' }]
});
};
为什么这个 Hooks 值得复用?
- 减少重复代码:将通用逻辑抽象,每个图表只需关注配置和业务逻辑
- 边界处理完善:包含容器不存在、重复初始化等异常情况处理
- 灵活性高:支持多种容器形式,适应不同场景
- 内存安全:自动清理事件和实例,避免内存泄漏
- 响应式友好:完美配合 Vue 的响应式系统,数据变化自动更新图表
最后
这个useEchart Hooks 已经在我们项目中大规模使用,极大提升了开发效率。如果你也经常和 ECharts 打交道,不妨试试这个封装思路,也可以根据自己的需求扩展更多功能(比如主题切换、加载状态等)。
完整代码已经放在开头,直接复制就能用,有任何优化建议欢迎在评论区交流~
觉得有用的话别忘了点赞收藏,关注我获取更多前端实用工具封装技巧!