【Vue3】我用 Vue 封装了个 ECharts Hooks,同事看了直接拿去复用

前言

在前端开发中,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 值得复用?

  1. 减少重复代码:将通用逻辑抽象,每个图表只需关注配置和业务逻辑
  2. 边界处理完善:包含容器不存在、重复初始化等异常情况处理
  3. 灵活性高:支持多种容器形式,适应不同场景
  4. 内存安全:自动清理事件和实例,避免内存泄漏
  5. 响应式友好:完美配合 Vue 的响应式系统,数据变化自动更新图表

最后

这个useEchart Hooks 已经在我们项目中大规模使用,极大提升了开发效率。如果你也经常和 ECharts 打交道,不妨试试这个封装思路,也可以根据自己的需求扩展更多功能(比如主题切换、加载状态等)。

完整代码已经放在开头,直接复制就能用,有任何优化建议欢迎在评论区交流~

觉得有用的话别忘了点赞收藏,关注我获取更多前端实用工具封装技巧!

相关推荐
wordbaby27 分钟前
React Native 进阶实战:基于 Server-Driven UI 的动态表单架构设计
前端·react native·react.js
风止何安啊29 分钟前
JS 里的 “变量租房记”:闭包是咋把变量 “扣” 下来的?
前端·javascript·node.js
老华带你飞32 分钟前
社区养老保障|智慧养老|基于springboot+小程序社区养老保障系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·小程序·毕设·社区养老保障
Danny_FD34 分钟前
用 ECharts markLine 标注节假日
前端·echarts
程序员西西35 分钟前
SpringBoot无感刷新Token实战指南
java·开发语言·前端·后端·计算机·程序员
烛阴35 分钟前
Luban集成CocosCreator完整教程
前端·typescript·cocos creator
有点笨的蛋36 分钟前
深入理解 JavaScript 原型机制:构造函数、原型对象与原型链
前端·javascript
o***741737 分钟前
spring-boot-starter和spring-boot-starter-web的关联
前端
晴栀ay39 分钟前
JS中原型式面向对象的精髓
前端·javascript