一、前言
在中后台系统、数据大屏、可视化平台开发中,ECharts 图表是刚需功能。如果每个页面都单独初始化、监听窗口、手动更新数据、处理销毁逻辑,会产生大量重复冗余代码,且极易出现以下问题:
- 页面切换未销毁实例,导致 内存泄漏
- 窗口频繁 resize 造成 卡顿、频繁重绘
- 数据更新无法响应式自动刷新
- 多图表页面 resize 事件冲突、性能浪费
- 无类型约束,参数传参混乱、隐性 BUG 多
本文基于 Vue3 + TypeScript 封装一套生产级、高复用、全自动适配的 ECharts 通用组件。
组件能力全覆盖:
- ✅ 完整 TS 类型约束,杜绝隐式类型错误
- ✅ 自动监听窗口变化、自适应重绘
- ✅ 防抖 resize,极致性能优化
- ✅ 响应式 option 自动更新图表
- ✅ 组件销毁自动释放实例,防止内存泄漏
- ✅ 支持亮色/暗色主题切换
- ✅ 支持 loading 加载状态、空数据兜底
- ✅ 纯组件封装、零侵入业务、全局可复用
二、环境安装
当前项目基于 Vue3 + TS,安装最新版 ECharts:
csharp
# npm
npm install echarts
# yarn
yarn add echarts
# pnpm
pnpm add echarts
三、整体封装思路
我们将封装一个通用 BaseEcharts 基础图表组件,所有业务图表(折线图、柱状图、饼图)全部基于该组件实现:
- 通过 defineProps 定义规范入参(option、loading、theme、width、height)
- 组件挂载后初始化 ECharts 实例
- 监听 option 变化,自动更新图表
- 封装防抖 resize 函数,监听窗口变化
- 组件销毁时 dispose 销毁实例,释放内存
- 统一兜底样式、loading 状态、空数据展示
四、TS 类型定义封装
新建 types/echarts.d.ts,统一管理图表类型,让组件传参百分百类型安全。
csharp
import type { EChartsOption, EChartsTheme } from 'echarts'
/** 基础图表组件 Props 类型 */
export interface BaseEchartsProps {
/** 图表配置项 */
option: EChartsOption
/** 是否开启 loading 加载 */
loading?: boolean
/** 图表主题 */
theme?: EChartsTheme | string | null
/** 图表宽度 */
width?: string | number
/** 图表高度 */
height?: string | number
}
/** 图表组件事件类型 */
export interface BaseEchartsEmits {
(e: 'click', params: any): void
}
五、封装通用 BaseEcharts 组件(完整版 TS 源码)
新建全局通用组件 src/components/BaseEcharts/index.vue,整合防抖 resize、响应式更新、实例销毁、主题切换、Loading、点击事件抛出,生产级开箱即用。
xml
<template>
<div class="base-echarts" :style="chartStyle" ref="chartRef"></div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount, computed } from 'vue'
import * as echarts from 'echarts'
import type { ECharts } from 'echarts'
import type { BaseEchartsProps, BaseEchartsEmits } from '@/types/echarts'
// 定义属性与默认值
const props = withDefaults(defineProps<BaseEchartsProps>(), {
loading: false,
theme: null,
width: '100%',
height: '400px'
})
// 定义抛出事件
const emit = defineEmits<BaseEchartsEmits>()
// DOM实例 & 图表实例
const chartRef = ref<HTMLDivElement | null>(null)
const chartInstance = ref<ECharts | null>(null)
// 容器样式自适应
const chartStyle = computed(() => ({
width: props.width,
height: props.height
}))
/**
* 防抖函数
*/
function debounce(fn: () => void, delay = 100) {
let timer: number | null = null
return () => {
if (timer) clearTimeout(timer)
timer = window.setTimeout(() => {
fn()
timer = null
}, delay)
}
}
/**
* 初始化图表
*/
const initChart = () => {
if (!chartRef.value) return
// 存在实例先销毁,防止重复初始化
if (chartInstance.value) {
chartInstance.value.dispose()
}
// 初始化实例
chartInstance.value = echarts.init(chartRef.value, props.theme || undefined)
// 渲染配置
chartInstance.value.setOption(props.option)
// 绑定点击事件,向外抛出
chartInstance.value.on('click', (params) => {
emit('click', params)
})
}
/**
* 自适应重绘
*/
const resizeChart = debounce(() => {
chartInstance.value?.resize()
}, 120)
/**
* 更新图表配置
*/
const updateOption = () => {
chartInstance.value?.setOption(props.option, true)
}
/**
* 开启/关闭 Loading
*/
watch(
() => props.loading,
(val) => {
chartInstance.value?.showLoading()
if (!val) {
chartInstance.value?.hideLoading()
}
}
)
// 监听配置项变化,自动更新图表
watch(
() => props.option,
() => {
updateOption()
},
{ deep: true }
)
// 监听主题切换,重新初始化图表
watch(
() => props.theme,
() => {
initChart()
}
)
// 挂载初始化 & 监听窗口
onMounted(() => {
initChart()
window.addEventListener('resize', resizeChart)
})
// 销毁实例,防止内存泄漏
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeChart)
if (chartInstance.value) {
chartInstance.value.dispose()
chartInstance.value = null
}
})
</script>
<style scoped>
.base-echarts {
box-sizing: border-box;
}
</style>
六、业务页面使用示例(TS 完整调用)
任意页面直接引入组件,传入 option 配置 即可快速渲染图表,自带自适应、Loading、点击事件,无需重复处理逻辑。
xml
<template>
<div class="chart-box">
<BaseEcharts
:option="chartOption"
:loading="loading"
height="420px"
@click="handleChartClick"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import BaseEcharts from '@/components/BaseEcharts/index.vue'
import type { EChartsOption } from 'echarts'
// 加载状态
const loading = ref(false)
// 图表配置
const chartOption = ref<EChartsOption>({
title: {
text: '月度数据统计',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月']
},
yAxis: {
type: 'value'
},
series: [
{
name: '访问量',
type: 'bar',
data: [120, 200, 150, 80, 70, 190]
}
]
})
// 图表点击事件
const handleChartClick = (params: any) => {
console.log('点击图表数据:', params)
}
</script>
<style scoped>
.chart-box {
width: 100%;
}
</style>
七、高级用法示例
7.1 主题切换(亮色/暗色)
ECharts 内置 dark 暗色主题,只需动态绑定 theme 属性即可无缝切换。
xml
<template>
<BaseEcharts :option="chartOption" :theme="chartTheme" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
const chartTheme = ref<string | null>(null)
// 切换暗色主题
const toggleDark = () => {
chartTheme.value = 'dark'
}
// 切换亮色主题
const toggleLight = () => {
chartTheme.value = null
}
</script>
7.2 动态更新图表数据
直接修改 chartOption 响应式数据,组件会自动监听更新,无需手动调用渲染方法。
ini
// 刷新图表数据
const refreshData = () => {
chartOption.value.series = [
{
name: '访问量',
type: 'bar',
data: [180, 160, 220, 90, 130, 200]
}
]
}
八、高频实用进阶代码示例(生产常用)
基于上述通用 BaseEcharts 组件,拓展4个项目开发中高频刚需的 TS 实战示例,涵盖打包优化、空状态兜底、动态图例切换、渐变可视化样式,直接复制可用。
8.1 ECharts 按需引入(极致打包体积优化)
默认全量引入 ECharts 体积较大,生产环境推荐按需引入图表、组件,大幅缩减项目打包体积,TS 完全适配。替换组件内的 ECharts 引入方式即可。
typescript
// 按需引入核心模块、图表类型、组件
import type { EChartsOption, ECharts } from 'echarts'
import * as echarts from 'echarts/core'
import { BarChart, LineChart, PieChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent
} from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
// 注册所需模块
echarts.use([
BarChart,
LineChart,
PieChart,
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
CanvasRenderer
])
优势:仅打包项目用到的图表和组件,打包体积减少 60%+,适配后台管理系统、轻量化项目。
8.2 空数据兜底展示(生产必备)
原生 ECharts 空数据会空白无提示,用户体验极差。通过 TS 逻辑判断,实现空数据自定义文案兜底。
css
import { ref, computed } from 'vue'
import type { EChartsOption } from 'echarts'
// 模拟接口数据
const tableData = ref<number[]>([])
// 自适应处理空数据
const chartOption = computed<EChartsOption>(() => {
// 无数据展示兜底文案
if (!tableData.value.length) {
return {
title: {
text: '暂无数据',
left: 'center',
top: 'center',
textStyle: {
color: '#999',
fontSize: 14
}
},
xAxis: { show: false },
yAxis: { show: false },
series: []
}
}
// 有数据正常渲染图表
return {
title: { text: '月度数据统计', left: 'center' },
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月']
},
yAxis: { type: 'value' },
series: [{
name: '访问量',
type: 'bar',
data: tableData.value
}]
}
})
8.3 动态图例切换/显隐(交互进阶)
业务中常需要点击按钮控制图例显隐、切换展示数据,基于响应式 option 实现全自动更新,TS 类型无报错。
xml
<template>
<div>
<button @click="toggleSeries">切换数据展示</button>
<BaseEcharts :option="chartOption" height="400px" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import BaseEcharts from '@/components/BaseEcharts/index.vue'
import type { EChartsOption } from 'echarts'
// 控制切换状态
const showLine = ref(true)
// 图表配置响应式更新
const chartOption = ref<EChartsOption>({
tooltip: { trigger: 'axis' },
legend: { data: ['访问量', '成交量'] },
xAxis: { data: ['1月', '2月', '3月', '4月', '5月', '6月'] },
yAxis: {},
series: [
{ name: '访问量', type: 'bar', data: [120, 200, 150, 80, 70, 190] },
{ name: '成交量', type: 'line', data: [80, 150, 120, 200, 90, 170] }
]
})
// 动态切换图例数据
const toggleSeries = () => {
showLine.value = !showLine.value
// 动态修改series,组件自动更新图表
chartOption.value.series = showLine.value
? [
{ name: '访问量', type: 'bar', data: [120, 200, 150, 80, 70, 190] },
{ name: '成交量', type: 'line', data: [80, 150, 120, 200, 90, 170] }
]
: [{ name: '访问量', type: 'bar', data: [120, 200, 150, 80, 70, 190] }]
}
</script>
8.4 渐变色彩图表(大屏可视化常用)
数据大屏、可视化页面高频使用渐变柱状图/折线图,相比默认纯色样式更加高级美观。下面提供完整 TS 可用渐变图表配置代码,开箱即用。
css
import { ref } from 'vue'
import type { EChartsOption } from 'echarts'
const chartOption = ref<EChartsOption>({
title: {
text: '渐变数据统计图表',
left: 'center'
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0,0,0,0.6)'
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
axisLine: { lineStyle: { color: '#eee' } }
},
yAxis: { type: 'value' },
series: [
{
name: '访问量',
type: 'bar',
data: [120, 200, 150, 80, 70, 190],
// 渐变颜色核心配置
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: '#409EFF' },
{ offset: 1, color: '#79BBFF' }
]
},
borderRadius: 6
}
}
]
})
数据大屏、可视化页面高频使用渐变柱状图/折线图,封装通用渐变配置,TS 完整适配,样式高级美观。
默认全量导入 ECharts 会引入所有图表、组件、渲染器,体积庞大,严重增加项目打包体积、拖慢首屏加载速度。生产环境必须使用按需引入,仅打包业务用到的图表与组件,体积可缩减 60%+。
改造方式:替换通用组件内的 ECharts 导入逻辑,完整 TS 类型兼容,无类型报错。
typescript
// 【替换组件顶部全部echarts导入代码】
import { ref, watch, onMounted, onBeforeUnmount, computed } from 'vue'
import type { EChartsOption, ECharts } from 'echarts'
// 核心按需引入模块
import * as echarts from 'echarts/core'
// 按需引入需要的图表类型
import { BarChart, LineChart, PieChart } from 'echarts/charts'
// 按需引入需要的功能组件
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent
} from 'echarts/components'
// 必须引入画布渲染器(核心渲染依赖)
import { CanvasRenderer } from 'echarts/renderers'
// 全局注册所有用到的模块
echarts.use([
BarChart,
LineChart,
PieChart,
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
CanvasRenderer
])
// 类型声明、组件逻辑无需改动
import type { BaseEchartsProps, BaseEchartsEmits } from '@/types/echarts'
拓展适配 :如果业务需要地图、雷达图、仪表盘等,只需对应引入模块并在 echarts.use() 中注册即可,TS 自动适配类型。
十、核心封装优势总结
- 完整 TS 类型约束:Props、事件全部类型定义,编码智能提示,杜绝传参错误、隐性BUG
- 彻底解决内存泄漏:组件卸载自动移除窗口监听、销毁 ECharts 实例,释放内存
- 防抖高性能自适应:窗口 resize 防抖处理,避免高频重绘造成页面卡顿
- 全自动响应式更新:option、loading、theme 变更自动刷新图表,无需手动调用方法
- 低侵入高复用:底层逻辑统一封装,零业务侵入,所有图表场景通用
- 功能全覆盖:支持 Loading、主题切换、点击事件、动态宽高、空数据兜底、渐变样式
- 打包体积优化:支持按需引入,极致精简项目打包体积,适配生产环境
十一、生产落地避坑注意事项(TS 专属)
11.1 禁止重复初始化实例
使用 v-if 条件渲染、弹窗切换图表时,会频繁触发组件挂载销毁,容易重复初始化多个 ECharts 实例,导致图表错乱、内存溢出。组件内部已做实例判断,存在旧实例则先销毁再创建,彻底规避该问题。
11.2 监听 option 必须开启 deep 深度监听
option 为复杂嵌套对象,Vue 浅层监听无法识别对象内部属性修改。必须配置 deep: true,否则动态修改图表数据、样式后,视图不会更新。
11.3 弹窗/隐藏容器图表延迟重绘
弹窗、抽屉、折叠面板默认 display: none,DOM 渲染后无宽高,初始化图表会出现尺寸为 0、空白不显示的问题。解决方案:在弹窗完全展开后的生命周期中,手动调用 chartInstance.value?.resize() 重绘图表。
scss
// 弹窗打开完成后重绘
const handleDialogOpened = () => {
nextTick(() => {
chartInstance.value?.resize()
})
}
11.4 组件销毁必须手动 dispose
ECharts 实例不会随 Vue 组件销毁自动释放,若不手动调用 dispose(),会造成实例常驻内存,多页面跳转、频繁开关弹窗后,内存持续升高,导致浏览器卡顿、页面卡死。
11.5 TS 自定义类型缺失适配方案
使用自定义图表、自定义图例、特殊配置时,会出现 TS 类型缺失报错。优先在 echarts.d.ts 扩展全局类型,临时开发可使用类型断言兜底,不建议长期使用:
csharp
// 临时兜底写法(开发调试用)
const customOption = ref({
// 自定义特殊配置
} as unknown as EChartsOption)
11.6 按需引入模块缺失报错解决
按需引入后,若出现 option配置无效、图表不渲染问题,大概率是未注册对应模块 。用到什么图表/组件,必须手动引入并在 echarts.use() 注册,否则功能失效。