普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月9日首页

ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)

作者 Ticnix
2026年2月9日 15:56

前言

Echarts作为一款功能强大的数据可视化库,具备丰富的图表类型、配置化开发的易用性、高度可定制的视觉效果、优秀的响应式设计和交互体验,以及对大数据量的性能优化能力,广泛应用于企业管理系统、数据分析平台等场景。

但每创建一个图表都要处理初始化、销毁、resize 适配通用逻辑,项目如果体积大起来,创建和维护就变得特别麻烦,我们直接看官网对于图表的创建的快速上手:快速上手 - 使用手册 - Apache ECharts

image.png

如果要调整图表的自适应大小还要:

image.png

最后每次离开页面还要销毁实例,实在是太麻烦了。。。

如果可以把Echarts这些烦人的重复性步骤封装起来,只传我需要的自定义配置参数就很方便了

其实封装起来的原理很简单,就是换汤不换药,把最重要的芯子挖空就行,然后用到的时候再把芯子装回去,装不同额芯子就能实现不一样的效果,这样免去了从头到尾创建的过程,用起来十分方便,而且维护起来只用维护一个组件就好了。

使用教程

到底有多方便?直接上食用方法

在template里使用组件

先把ECharts组件写进components里封装再在要用到的页面使用

<ECharts                                               
    width="600px"                      <!-- 图表容器宽度,支持像素值或百分比 -->
    height="400px"                     <!-- 图表容器高度,支持像素值或百分比 -->
    element="salaryChart"               <!-- 图表元素 ID(每个图表唯一) -->
    :option="salaryChartOption"         <!-- 图表配置选项,包含数据、样式等 -->
    :function-type="1"                  <!-- 功能类型:0=无交互,1=点击+高亮,2=点击+对话框,12=两者都有 -->
    @chart-event="handleChartEvent"     <!-- 图表事件处理函数,接收点击事件参数 -->
/>

在js配置参数

js里就直接写对应的导入、配置参数、点击事件就好了

这里的option配置参数具体参考官方文档的option配置项写法 Documentation - Apache ECharts

点击事件参考官方的 事件与行为 - 概念篇 - 使用手册 - Apache ECharts

import ECharts from '@/components/ECharts.vue';

// 薪资分布图表配置
const salaryChartOption = {
  title: {
    text: '员工薪资分布',
    left: 'center'
  },
  tooltip: {
    trigger: 'axis',
    axisPointer: {
      type: 'shadow'
    }
  },
  grid: {
    left: '3%',
    right: '4%',
    bottom: '3%',
    containLabel: true
  },
  xAxis: {
    type: 'category',
    data: sampleData.map(item => item.name),
    axisLabel: {
      rotate: 45
    }
  },
  yAxis: {
    type: 'value',
    name: '薪资(元)'
  },
  series: [
    {
      name: '薪资',
      type: 'bar',
      data: sampleData.map(item => item.salary),
      itemStyle: {
        color: '#188df0'
      },
      emphasis: {
        itemStyle: {
          color: '#2378f7'
        }
      }
    }
  ]
};

// 处理图表事件
const handleChartEvent = (params: any) => {
  console.log('图表事件:', params);
  showMessage(`你点击了:${params.name || params.data.name}`, 'success');
};

组件封装

ECharts 组件封装的完整过程可概括为:

  1. 首先搭建组件基础结构,包括模板、脚本和样式;

  2. 接着定义类型和 Props 配置,确保类型安全和使用灵活性;

  3. 然后实现图表实例管理,包括创建、配置和销毁;通过响应式更新机制,实现图表配置的自动更新;添加事件处理与交互,支持与父组件的通信;在生命周期管理中,确保图表正确初始化和清理;

  4. 通过性能优化措施,提升组件性能;

  5. 暴露公共方法,支持更灵活的操作;最后添加错误处理与日志,增强组件健壮性。

1. 组件基础结构搭建

核心目标 :创建组件的基本框架,包括模板、脚本和样式

实现细节 :

  • 模板部分 :使用 div 作为图表容器,通过 ref 获取 DOM 元素引用,设置动态宽高样式
  • 脚本部分 :采用 Vue 3 的
  • 样式部分 :使用 scoped 样式,确保样式隔离,设置基本容器样式和过渡效果
<template>
  <div ref="chartRef" :style="{ height: height, width: width }" class="echarts-container" />
</template>

<script setup lang="ts">
// 后续逻辑实现
</script>

<style scoped>
.echarts-container {
  position: relative;
  box-sizing: border-box;
  min-width: 300px;
  min-height: 300px;
  transition: width 0.3s ease, height 0.3s ease;
}
</style>

2. 与 Props 配置

核心目标 :定义组件的属性类型和默认值,确保类型安全和使用灵活性。

实现细节 :

  • 类型定义 :使用 interface Props 定义组件属性类型,包含宽高、配置项、主题等
  • 默认值设置 :通过 withDefaults(defineProps(), {...}) 设置默认值
  • 类型导入 :导入 ECharts 相关类型(如 EChartsOption 、 ECElementEvent )
// 定义props
interface Props {
  width?: string | number;
  height?: string | number;
  option: EChartsOption;
  functionType?: number;
  debounceDelay?: number;
  theme?: string | null;
  initOpts?: EChartsInitOpts;
  autoResize?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  width: '100%',
  height: '400px',
  functionType: 0,
  debounceDelay: 300,
  theme: null,
  initOpts: () => ({
    devicePixelRatio: window.devicePixelRatio || 1,
    renderer: 'canvas'
  }),
  autoResize: true
});

3. 图表实例管理

核心目标 :创建和管理 ECharts 实例,确保实例的正确初始化和销毁

实现细节 :

  • 实例存储 :使用 let chartInstance: ECharts | null = null 存储图表实例
  • DOM 引用 :使用 const chartRef = ref<HTMLElement | null>(null) 获取图表容器元素
  • 实例创建 :在 initChart 函数中使用 echarts.init() 创建实例
  • 实例销毁 :在组件卸载和重新初始化时使用 chartInstance.dispose() 销毁实例
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: ECharts | null = null;

// 初始化图表
const initChart = async (): Promise<void> => {
  try {
    // 清理现有实例
    if (chartInstance) {
      chartInstance.dispose();
      chartInstance = null;
    }

    // 确保元素存在
    if (!chartRef.value) {
      throw new Error('图表容器元素不存在');
    }

    // 初始化图表实例
    chartInstance = echarts.init(
      chartRef.value,
      props.theme,
      props.initOpts
    );
    
    // 后续配置...
  } catch (error) {
    console.error('ECharts: 图表初始化失败', error);
    emit('error', error as Error);
  }
};

4. 响应式更新机制

核心目标 :实现图表配置的自动更新,当 props 变化时图表能相应调整。

实现细节 :

  • 配置监听 :使用 watch 监听 option 变化,自动调用 setOption 更新图表
  • 主题监听 :监听 theme 变化,触发重新初始化图表
  • 尺寸监听 :监听 width 和 height 变化,调用 resize 方法调整图表大小
  • 深度监听 :对 option 使用 deep: true 确保嵌套属性变化也能被检测到
// 监听配置变化
watch(
  () => props.option,
  (newOption) => {
    if (newOption && chartInstance) {
      chartInstance.setOption(newOption, true);
    }
  },
  { deep: true, immediate: false }
);

// 监听主题变化
watch(
  () => props.theme,
  () => {
    initChart();
  }
);

// 监听尺寸变化
watch(
  [() => props.width, () => props.height],
  () => {
    resize();
  }
);

5. 事件处理与交互

核心目标 :实现图表的事件绑定和处理,支持与父组件的交互。

实现细节 :

  • 事件定义 :使用 defineEmits 定义组件可触发的事件(如 chart-event 、 init 、 error )
  • 事件绑定 :在 bindEvents 函数中根据 functionType 绑定不同的点击事件
  • 事件处理 :实现 handleClickEvent 和 handleDialogEvent 处理具体事件逻辑
  • 事件传递 :通过 emit 将事件参数传递给父组件
const emit = defineEmits<{
  'chart-event': [params: ECElementEvent];
  'init': [instance: ECharts];
  'error': [error: Error];
}>();

// 绑定事件
const bindEvents = (): void => {
  if (!chartInstance) return;

  // 移除现有事件监听
  chartInstance.off('click');

  // 根据 functionType 绑定不同事件
  if (props.functionType === 1 || props.functionType === 12) {
    chartInstance.on('click', handleClickEvent);
  } else if (props.functionType === 2 || props.functionType === 12) {
    chartInstance.on('click', handleDialogEvent);
  }
};

6. 生命周期管理

核心目标 :在组件的生命周期不同阶段执行相应的操作,确保图表正确初始化和清理

实现细节 :

  • 组件挂载 :在 onMounted 中初始化图表并添加窗口 resize 事件监听
  • 延迟初始化 :使用 setTimeout 确保 DOM 完全加载后再初始化图表
  • 组件卸载 :在 onBeforeUnmount 中清理事件监听器、定时器和销毁图表实例
onMounted(async () => {
  // 延迟初始化,确保 DOM 完全加载
  setTimeout(async () => {
    await initChart();
  }, 100);

  // 添加窗口 resize 事件监听
  if (props.autoResize) {
    window.addEventListener('resize', debouncedResize);
  }
});

onBeforeUnmount(() => {
  // 清理窗口 resize 事件监听
  if (props.autoResize) {
    window.removeEventListener('resize', debouncedResize);
  }

  // 清理定时器
  if (resizeTimer) {
    clearTimeout(resizeTimer);
  }

  // 销毁图表实例
  if (chartInstance) {
    chartInstance.dispose();
    chartInstance = null;
  }
});

7. 性能优化措施

核心目标 :通过优化手段提升组件性能,减少不必要的计算和渲染。

实现细节 :

  • 防抖处理 :实现 debounce 函数处理窗口 resize 事件,避免频繁触发
  • 合理初始化 :只在必要时重新初始化图表(如主题变化)
  • 资源清理 :在组件卸载时彻底清理资源,防止内存泄漏
  • 条件执行 :在事件绑定和方法调用前检查实例是否存在
// 防抖函数
const debounce = <T extends (...args: any[]) => any>(
  func: T,
  delay: number
): ((...args: Parameters<T>) => void) => {
  let timer: ReturnType<typeof setTimeout> | null = null;
  return (...args: Parameters<T>) => {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      func(...args);
      timer = null;
    }, delay);
  };
};

// 防抖处理的 resize 函数
const debouncedResize = debounce(resize, props.debounceDelay);    

8. 公共方法暴露

核心目标 :将图表实例的方法暴露给父组件,支持更灵活的操作。

实现细节 :

  • 方法定义 :实现常用的图表操作方法(如 resize 、 setOption 、 dispatchAction 等)
  • 方法暴露 :使用 defineExpose 将这些方法暴露给父组件
  • 实例获取 :提供 getInstance 方法,允许父组件直接获取 ECharts 实例
// 重新渲染图表
const resize = (): void => {
  if (chartInstance) {
    chartInstance.resize();
  }
};

// 获取图表实例
const getInstance = (): ECharts | null => {
  return chartInstance;
};

// 设置图表配置
const setOption = (option: EChartsOption, notMerge?: boolean): void => {
  if (chartInstance) {
    chartInstance.setOption(option, notMerge);
  }
};

// 暴露方法
defineExpose({
  resize,
  getInstance,
  setOption,
  dispatchAction,
  clear,
  showLoading,
  hideLoading
});

完整封装代码(直接CV可食用)

通过集中处理初始化、销毁、resize 适配等通用逻辑,实现代码复用,避免重复编写实现细节;提升维护性,修改时只需更新组件代码,所有使用处自动受益;保证接口一致性,团队成员可通过统一的 props 和事件快速集成;同时便于功能扩展(如防抖处理、错误捕获)和提升代码可读性,使父组件更专注于业务逻辑和图表配置,最终实现更高效、可靠的数据可视化方案。

<template>
  <div ref="chartRef" :style="{ height: height, width: width }" class="echarts-container" />
</template>

<script setup lang="ts">
import {ref, watch, onMounted, onBeforeUnmount, computed} from 'vue';
import * as echarts from 'echarts';
import type {ECharts, EChartsOption, ECElementEvent, EChartsInitOpts} from 'echarts';

// 定义props
interface Props {
  width?: string | number;
  height?: string | number;
  option: EChartsOption;
  functionType?: number;
  debounceDelay?: number;
  theme?: string | null;
  initOpts?: EChartsInitOpts;
  autoResize?: boolean;
}

const emit = defineEmits<{
  'chart-event': [params: ECElementEvent];
  'init': [instance: ECharts];
  'error': [error: Error];
}>();

// 暴露方法将在所有函数定义后添加

const props = withDefaults(defineProps<Props>(), {
  width: '100%',
  height: '400px',
  functionType: 0,
  debounceDelay: 300,
  theme: null,
  initOpts: () => ({
    devicePixelRatio: window.devicePixelRatio || 1,
    renderer: 'canvas'
  }),
  autoResize: true
});

const chartRef = ref<HTMLElement | null>(null);
let chartInstance: ECharts | null = null;
const resizeTimer: ReturnType<typeof setTimeout> | null = null;

// 计算宽度和高度
const computedWidth = computed(() => {
  return typeof props.width === 'number' ? `${props.width}px` : props.width;
});

const computedHeight = computed(() => {
  return typeof props.height === 'number' ? `${props.height}px` : props.height;
});

// 防抖函数
const debounce = <T extends (...args: any[]) => any>(
  func: T,
  delay: number
): ((...args: Parameters<T>) => void) => {
  let timer: ReturnType<typeof setTimeout> | null = null;
  return (...args: Parameters<T>) => {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      func(...args);
      timer = null;
    }, delay);
  };
};

// 初始化图表
const initChart = async (): Promise<void> => {
  try {
    console.log('ECharts: 开始初始化图表');

    // 清理现有实例
    if (chartInstance) {
      chartInstance.dispose();
      chartInstance = null;
    }

    // 确保元素存在
    if (!chartRef.value) {
      throw new Error('图表容器元素不存在');
    }

    // 检查元素尺寸
    const { offsetWidth, offsetHeight } = chartRef.value;
    if (offsetWidth === 0 || offsetHeight === 0) {
      throw new Error('图表容器尺寸为0,请检查容器样式');
    }

    console.log('ECharts: 图表容器尺寸', { width: offsetWidth, height: offsetHeight });

    // 初始化图表实例
    chartInstance = echarts.init(
      chartRef.value,
      props.theme,
      props.initOpts
    );

    console.log('ECharts: 图表实例创建成功', chartInstance);

    // 绑定事件
    bindEvents();

    // 设置图表配置
    if (props.option) {
      chartInstance.setOption(props.option, true);
      console.log('ECharts: 图表配置设置成功');
    }

    // 触发初始化完成事件
    emit('init', chartInstance);

    console.log('ECharts: 图表初始化完成');
  } catch (error) {
    console.error('ECharts: 图表初始化失败', error);
    emit('error', error as Error);
  }
};

// 绑定事件
const bindEvents = (): void => {
  if (!chartInstance) return;

  // 移除现有事件监听
  chartInstance.off('click');

  // 根据 functionType 绑定不同事件
  if (props.functionType === 1 || props.functionType === 12) {
    chartInstance.on('click', handleClickEvent);
  } else if (props.functionType === 2 || props.functionType === 12) {
    chartInstance.on('click', handleDialogEvent);
  }
};

// 处理点击事件
const handleClickEvent = (params: ECElementEvent): void => {
  console.log('ECharts: 点击事件触发', params);

  // 高亮点击的数据点
  if (chartInstance && params.seriesIndex !== undefined && params.dataIndex !== undefined) {
    chartInstance.dispatchAction({
      type: 'highlight',
      seriesIndex: params.seriesIndex,
      dataIndex: params.dataIndex
    });
  }

  // 触发自定义事件
  emit('chart-event', params);
};

// 处理对话框事件
const handleDialogEvent = (params: ECElementEvent): void => {
  console.log('ECharts: 对话框事件触发', params);
  emit('chart-event', params);
};

// 重新渲染图表
const resize = (): void => {
  if (chartInstance) {
    chartInstance.resize();
    console.log('ECharts: 图表尺寸调整');
  }
};

// 防抖处理的 resize 函数
const debouncedResize = debounce(resize, props.debounceDelay);

// 获取图表实例
const getInstance = (): ECharts | null => {
  return chartInstance;
};

// 设置图表配置
const setOption = (option: EChartsOption, notMerge?: boolean): void => {
  if (chartInstance) {
    chartInstance.setOption(option, notMerge);
    console.log('ECharts: 手动设置图表配置');
  }
};

// 触发图表动作
const dispatchAction = (action: echarts.Action): void => {
  if (chartInstance) {
    chartInstance.dispatchAction(action);
    console.log('ECharts: 触发图表动作', action);
  }
};

// 清空图表
const clear = (): void => {
  if (chartInstance) {
    chartInstance.clear();
    console.log('ECharts: 清空图表');
  }
};

// 显示加载动画
const showLoading = (type?: string, options?: echarts.LoadingOption): void => {
  if (chartInstance) {
    chartInstance.showLoading(type, options);
    console.log('ECharts: 显示加载动画');
  }
};

// 隐藏加载动画
const hideLoading = (): void => {
  if (chartInstance) {
    chartInstance.hideLoading();
    console.log('ECharts: 隐藏加载动画');
  }
};

// 暴露方法
defineExpose({
  resize,
  getInstance,
  setOption,
  dispatchAction,
  clear,
  showLoading,
  hideLoading
});

// 监听配置变化
watch(
  () => props.option,
  (newOption) => {
    if (newOption && chartInstance) {
      console.log('ECharts: 图表配置变化,更新图表');
      chartInstance.setOption(newOption, true);
    }
  },
  { deep: true, immediate: false }
);

// 监听主题变化
watch(
  () => props.theme,
  () => {
    console.log('ECharts: 图表主题变化,重新初始化图表');
    initChart();
  }
);

// 监听尺寸变化
watch(
  [() => props.width, () => props.height],
  () => {
    console.log('ECharts: 图表尺寸变化,调整图表');
    resize();
  }
);

onMounted(async () => {
  console.log('ECharts: 组件挂载');

  // 延迟初始化,确保 DOM 完全加载
  setTimeout(async () => {
    await initChart();
  }, 100);

  // 添加窗口 resize 事件监听
  if (props.autoResize) {
    window.addEventListener('resize', debouncedResize);
    console.log('ECharts: 添加窗口 resize 事件监听');
  }
});

onBeforeUnmount(() => {
  console.log('ECharts: 组件卸载');

  // 清理窗口 resize 事件监听
  if (props.autoResize) {
    window.removeEventListener('resize', debouncedResize);
  }

  // 清理定时器
  if (resizeTimer) {
    clearTimeout(resizeTimer);
  }

  // 销毁图表实例
  if (chartInstance) {
    chartInstance.dispose();
    chartInstance = null;
  }
});
</script>

<style scoped>
.echarts-container {
  position: relative;
  box-sizing: border-box;
  min-width: 300px;
  min-height: 300px;
  transition: width 0.3s ease, height 0.3s ease;
}
</style>

谢谢观看!

❌
❌