自定义封装 ECharts 组件及响应式调整
概述
在不使用 vue-echarts 等第三方库的情况下,可以直接封装原生 ECharts 的 Vue 组件。由于后续会将 ECharts 作为 CDN 外部依赖,采用全局导入 echarts 实例的方式初始化图表。封装过程中需要关注四个核心问题:实例管理、属性变更、窗口自适应、资源销毁。
封装要点
| 关注点 | 说明 |
|---|---|
| 实例初始化 | echarts.init(dom) 创建图表实例 |
| 属性变更 | watch 监听 option 变化,调用 setOption 更新 |
| 窗口自适应 | 监听容器尺寸变化,调用 chart.resize() |
| 资源销毁 | onUnmounted 时调用 chart.dispose() 释放资源 |
| 事件绑定 | 通过 chart.on() 绑定交互事件并暴露给父组件 |
组件实现
完整代码
<!-- components/charts/Charts.vue -->
<template>
<div ref="chartRef" class="charts-container" :style="{ width, height }" />
</template>
<script lang="ts" setup>
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import type { EChartsOption, EChartsType } from 'echarts'
interface ChartsProps {
option: EChartsOption
width?: string
height?: string
theme?: string | object
}
interface ChartsEmits {
(e: 'chartInit', chart: EChartsType): void
(e: 'click', params: any): void
}
const props = withDefaults(defineProps<ChartsProps>(), {
width: '100%',
height: '400px',
theme: undefined
})
const emit = defineEmits<ChartsEmits>()
const chartRef = ref<HTMLElement>()
let chartInstance: EChartsType | null = null
let resizeObserver: ResizeObserver | null = null
/**
* 初始化图表实例
*/
function initChart() {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value, props.theme)
chartInstance.setOption(props.option)
// 绑定点击事件
chartInstance.on('click', (params) => {
emit('click', params)
})
emit('chartInit', chartInstance)
}
/**
* 响应式调整:使用 ResizeObserver 监听容器尺寸变化
* 比监听 window.resize 更精确,适用于非窗口级别的尺寸变化
*/
function setupResizeObserver() {
if (!chartRef.value) return
resizeObserver = new ResizeObserver(() => {
chartInstance?.resize()
})
resizeObserver.observe(chartRef.value)
}
/**
* 销毁图表实例和观察器
*/
function dispose() {
resizeObserver?.disconnect()
resizeObserver = null
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
}
// 监听 option 变化,更新图表
watch(
() => props.option,
(newOption) => {
if (chartInstance) {
chartInstance.setOption(newOption, { notMerge: false })
}
},
{ deep: true }
)
onMounted(async () => {
await nextTick()
initChart()
setupResizeObserver()
})
onUnmounted(() => {
dispose()
})
// 暴露实例方法供父组件调用
defineExpose({
getInstance: () => chartInstance,
resize: () => chartInstance?.resize(),
dispose
})
</script>
<style scoped>
.charts-container {
min-height: 200px;
}
</style>
vue
使用方式
基础用法
<template>
<Charts
:option="chartOption"
height="300px"
@chart-init="onChartInit"
@click="onChartClick"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import Charts from '@/components/charts/Charts.vue'
import type { EChartsOption, EChartsType } from 'echarts'
const chartOption = ref<EChartsOption>({
title: { text: '用户统计' },
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: { type: 'value' },
series: [
{
name: '活跃用户',
type: 'line',
data: [150, 230, 224, 218, 135, 147, 260]
}
]
})
function onChartInit(chart: EChartsType) {
console.log('Chart initialized:', chart)
}
function onChartClick(params: any) {
console.log('Clicked:', params)
}
</script>
vue
动态数据更新
// 动态更新图表数据
function updateChartData(newData: number[]) {
chartOption.value = {
...chartOption.value,
series: [
{
name: '活跃用户',
type: 'line',
data: newData
}
]
}
}
typescript
自适应方案对比
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
window.resize | 固定容器 | 简单直接 | 无法检测容器自身尺寸变化 |
ResizeObserver | 弹性布局 | 精确检测容器变化 | 需要浏览器支持(IE 不支持) |
v-chart 自动 resize | vue-echarts | 开箱即用 | 引入额外依赖 |
推荐使用 ResizeObserver
// ResizeObserver 比 window.resize 更精确
// 当侧边栏折叠、抽屉打开等场景下容器尺寸变化时,仍能正确 resize
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
chartInstance?.resize()
}
}
})
resizeObserver.observe(containerEl)
typescript
主题支持
// 注册自定义主题
import * as echarts from 'echarts'
echarts.registerTheme('dark', {
backgroundColor: '#1a1a2e',
// ...主题配置
})
// 使用主题
// <Charts :option="option" theme="dark" />
typescript
setOption 参数说明
chartInstance.setOption(option, {
notMerge: false, // 是否不合并之前的 option,默认 false(合并)
lazyUpdate: false, // 是否延迟更新,默认 false
silent: false, // 是否阻止事件触发
})
// notMerge: true → 完全替换,适合切换图表类型
// notMerge: false → 增量合并,适合数据更新(推荐)
typescript
实践要点
- 使用
ResizeObserver替代window.resize监听,更精确地响应容器尺寸变化 watch监听option时必须设置{ deep: true },否则嵌套属性变更无法触发更新onUnmounted中务必调用chart.dispose()释放内存,避免组件频繁切换导致内存泄漏- 通过
defineExpose暴露实例方法,支持父组件直接调用 ECharts API - CDN 外部化 ECharts 时,
import * as echarts from 'echarts'会被 Vite 自动替换为全局变量
↑