【Vue3】我用 Vue 封装了个 ECharts Hooks
<h1 data-id="heading-0">🧑💻 写在开头</h1><p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<div>
<div>
<h2 data-id="heading-0">前言</h2>
<blockquote>
<p>在前端开发中,ECharts 作为数据可视化的利器被广泛使用,但每次使用都要重复处理初始化、容器获取、事件绑定、窗口 resize 等逻辑,不仅繁琐还容易出错。最近我封装了一个<code>useEchart</code> Hooks,彻底解决了这些痛点,今天就来分享一下实现思路和使用技巧。</p>
</blockquote>
<h2 data-id="heading-1">为什么需要这个 Hooks?</h2>
<p>先看看我们平时用 ECharts 的常规操作:</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 常规写法
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();
});</pre>
</div>
<div>
<div>
<p>这段代码不算复杂,但每个图表都要写一遍就很折磨人了。更麻烦的是:</p>
<ul>
<li>容器获取要处理各种情况(DOM 元素、ID 选择器、Vue Ref)</li>
<li>频繁初始化容易导致内存泄漏</li>
<li>事件绑定 / 解绑需要手动管理</li>
<li>响应式数据更新要手动触发 setOption</li>
</ul>
<h2 data-id="heading-2">useEchart Hooks 来了!</h2>
<p>基于以上痛点,我封装了<code>useEchart</code> Hooks,核心功能包括:</p>
<ul>
<li>支持多种容器类型(Ref、DOM 元素、ID / 类选择器)</li>
<li>自动处理初始化与销毁</li>
<li>响应式配置更新</li>
<li>内置事件绑定 / 解绑方法</li>
<li>自动监听窗口 resize</li>
</ul>
<h3 data-id="heading-3">废话不多说先上代码!</h3>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">//先导入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;
let resizeObserver: ResizeObserver | null = null; //ResizeObserver 实例
// 辅助函数处理单个选择器
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");
resizeObserver = new ResizeObserver(() => {
chart?.resize();
});
resizeObserver.observe(containerElement);
}
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) {
// 清理 ResizeObserver 实例
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
chart.dispose();
chart = null;
}
});
return {
get chart() {
return chart;
},
onChartEvent,
offChartEvent,
update,
handleResize
};
}</pre>
</div>
<h3 data-id="heading-4">核心代码解析</h3>
<p>先看整体结构,这个 Hooks 主要包含这些部分:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">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 };
}</pre>
</div>
<h4 data-id="heading-5">1. 万能容器处理</h4>
<p>最实用的功能之一就是支持多种容器形式:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 支持的容器类型
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;
};</pre>
</div>
<p>无论是直接传 DOM 元素、Vue 的 Ref 对象,还是 ID 选择器(带 #或不带)、类选择器,甚至是选择器数组(自动取第一个匹配项),都能轻松处理。</p>
<h4 data-id="heading-6">2. 自动生命周期管理</h4>
<p>初始化逻辑会在组件挂载时执行,销毁时自动清理:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 初始化图表
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;
}
});</pre>
</div>
<p>再也不用担心忘记解绑事件或销毁实例导致的内存泄漏了!</p>
<h4 data-id="heading-7">3. 响应式与事件处理</h4>
<p>内置 watch 监听配置变化,自动更新图表:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 响应式更新图表配置
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);
};</pre>
</div>
<h2 data-id="heading-8">如何使用?</h2>
<p>用起来超级简单,三步到位:</p>
<h3 data-id="heading-9">1. 基础使用</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;"><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: , type: 'line' }]
});
// 初始化图表
const { chart, update } = useEchart(chartRef, option.value);
</script></pre>
</div>
<h3 data-id="heading-10">2. 事件绑定</h3>
<div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 绑定点击事件
const { onChartEvent } = useEchart(chartRef, option.value);
onChartEvent('click', (params) => {
console.log('点击了图表', params);
});</pre>
</div>
<h3 data-id="heading-11">3. 动态更新数据</h3>
<div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 直接更新配置
const { update } = useEchart(chartRef, option.value);
// 按钮点击更新数据
const handleUpdate = () => {
update({
series: [{ data: , type: 'line' }]
});
};</pre>
</div>
<div>
<div>
<h2 data-id="heading-12">为什么这个 Hooks 值得复用?</h2>
<ol>
<li><strong>减少重复代码</strong>:将通用逻辑抽象,每个图表只需关注配置和业务逻辑</li>
<li><strong>边界处理完善</strong>:包含容器不存在、重复初始化等异常情况处理</li>
<li><strong>灵活性高</strong>:支持多种容器形式,适应不同场景</li>
<li><strong>内存安全</strong>:自动清理事件和实例,避免内存泄漏</li>
<li><strong>响应式友好</strong>:完美配合 Vue 的响应式系统,数据变化自动更新图表</li>
</ol>
<h2 data-id="heading-13">最后</h2>
<p>这个<code>useEchart</code> Hooks 已经在我们项目中大规模使用,极大提升了开发效率。如果你也经常和 ECharts 打交道,不妨试试这个封装思路,也可以根据自己的需求扩展更多功能(比如主题切换、加载状态等)。</p>
<p>完整代码已经放在开头,直接复制就能用,有任何优化建议欢迎在评论区交流~</p>
</div>
</div>
</div>
<div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
</div>
<p><em><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></em></p>
</div>
</div>
</div><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19458682
頁:
[1]