广东缘来缘见 發表於 2026-1-8 20:48:00

【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>&nbsp;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(() =&gt; {
const dom = document.getElementById('chart-container');
if (dom) {
    chart = echarts.init(dom);
    chart.setOption(option);
    window.addEventListener('resize', handleResize);
}
});

// 更新数据
const updateChart = (newData) =&gt; {
if (chart) {
    chart.setOption({ series: [{ data: newData }] });
}
};

// 处理resize
const handleResize = () =&gt; {
chart?.resize();
};

// 销毁实例
onBeforeUnmount(() =&gt; {
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>&nbsp;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&lt;HTMLElement | null&gt;
| HTMLElement
| string
| string[];

/**
* 适配多种容器选择方式的 ECharts 封装
* @param container - 容器选择器(支持 Ref, DOM 元素, ID 选择器, 类选择器)
* @param option - 初始配置
* @returns {chart: echarts.ECharts | null, update: (newOption: EChartsOption) =&gt; void}
*/
export function useEchart(
container: container,
option: EchartsOption = {}
): {
chart: echarts.ECharts | null;
onChartEvent: (event: string, handler: (params: any) =&gt; void) =&gt; void;
offChartEvent: (event: string, handler: (params: any) =&gt; void) =&gt; void;
update: (newOption: EchartsOption) =&gt; void;
handleResize: () =&gt; void;
} {
let chart: echarts.ECharts | null = null;
let containerElement: HTMLElement | null = null;
let resizeObserver: ResizeObserver | null = null; //ResizeObserver 实例

//   辅助函数处理单个选择器
const getContainerElementForSingle = (
    selector: string
): HTMLElement | null =&gt; {
    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 =&gt; {
    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 =&gt; {
    containerElement = getContainerElement();

    if (!containerElement) {
      console.error("无法获取容器元素");
      return;
    }

    if (!chart) {
      chart = echarts.init(containerElement, "infographic");
      resizeObserver = new ResizeObserver(() =&gt; {
      chart?.resize();
      });
      resizeObserver.observe(containerElement);
    }

    if (option) {
      chart.setOption(option);
    }
};

//   处理窗口大小变化
const handleResize = () =&gt; {
    chart?.resize();
};

//   更新图表配置
const update = (newOption: EchartsOption): void =&gt; {
    if (chart) {
      chart.setOption(newOption);
    }
};
// 新增:事件绑定方法
const onChartEvent = (event: string, handler: (params: any) =&gt; void) =&gt; {
    chart?.on(event, handler);
};

const offChartEvent = (event: string, handler: (params: any) =&gt; void) =&gt; {
    chart?.off(event, handler);
};
//   响应式更新图表配置
watch(
    () =&gt; option,
    (newOption) =&gt; update(newOption),
    {
      deep: true,
    }
);

onMounted(() =&gt; {
    initChart();
});

onBeforeUnmount(() =&gt; {
    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 = () =&gt; { ... };

// 初始化图表
const initChart = () =&gt; { ... };

// 响应式更新
watch(() =&gt; option, (newOption) =&gt; { ... });

// 生命周期管理
onMounted(() =&gt; initChart());
onBeforeUnmount(() =&gt; { ... });

// 暴露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&lt;HTMLElement | null&gt; | HTMLElement | string | string[];

// 容器获取逻辑
const getContainerElement = () =&gt; {
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 = () =&gt; {
containerElement = getContainerElement();
if (!containerElement) {
    console.error("无法获取容器元素");
    return;
}

if (!chart) {
    chart = echarts.init(containerElement, "infographic");
    chart.resize();
    window.addEventListener("resize", handleResize);
}
chart.setOption(option);
};

// 组件卸载时清理
onBeforeUnmount(() =&gt; {
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(
() =&gt; option,
(newOption) =&gt; update(newOption),
{ deep: true }
);

// 事件绑定方法
const onChartEvent = (event: string, handler: (params: any) =&gt; void) =&gt; {
chart?.on(event, handler);
};

const offChartEvent = (event: string, handler: (params: any) =&gt; void) =&gt; {
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;">&lt;template&gt;
&lt;div ref="chartRef" class="chart-container"&gt;&lt;/div&gt;
&lt;/template&gt;

&lt;script setup&gt;
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);
&lt;/script&gt;</pre>
</div>
<h3 data-id="heading-10">2. 事件绑定</h3>
<div>&nbsp;
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 绑定点击事件
const { onChartEvent } = useEchart(chartRef, option.value);

onChartEvent('click', (params) =&gt; {
console.log('点击了图表', params);
});</pre>
</div>
<h3 data-id="heading-11">3. 动态更新数据</h3>
<div>&nbsp;
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 直接更新配置
const { update } = useEchart(chartRef, option.value);

// 按钮点击更新数据
const handleUpdate = () =&gt; {
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>&nbsp;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]
查看完整版本: 【Vue3】我用 Vue 封装了个 ECharts Hooks