博客 / 詳情

返回

【Vue3】我用 Vue 封裝了個 ECharts Hooks

🧑‍💻 寫在開頭

點贊 + 收藏 === 學會🤣🤣🤣

前言

在前端開發中,ECharts 作為數據可視化的利器被廣泛使用,但每次使用都要重複處理初始化、容器獲取、事件綁定、窗口 resize 等邏輯,不僅繁瑣還容易出錯。最近我封裝了一個useEchart Hooks,徹底解決了這些痛點,今天就來分享一下實現思路和使用技巧。

為什麼需要這個 Hooks?

先看看我們平時用 ECharts 的常規操作:

// 常規寫法
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

廢話不多説先上代碼!

//先導入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
  };
}

核心代碼解析

先看整體結構,這個 Hooks 主要包含這些部分:

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. 萬能容器處理

最實用的功能之一就是支持多種容器形式:

// 支持的容器類型
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. 自動生命週期管理

初始化邏輯會在組件掛載時執行,銷燬時自動清理:

// 初始化圖表
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 監聽配置變化,自動更新圖表:

// 響應式更新圖表配置
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. 基礎使用

<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. 事件綁定

 
// 綁定點擊事件
const { onChartEvent } = useEchart(chartRef, option.value);

onChartEvent('click', (params) => {
  console.log('點擊了圖表', params);
});

3. 動態更新數據

 
// 直接更新配置
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 打交道,不妨試試這個封裝思路,也可以根據自己的需求擴展更多功能(比如主題切換、加載狀態等)。

完整代碼已經放在開頭,直接複製就能用,有任何優化建議歡迎在評論區交流~

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.