引言
在现代Web开发中,ECharts凭借其丰富的图表类型和高度可定制性,已成为数据可视化领域的标杆工具。本文将基于Vue3框架,结合实际项目代码,详细解析如何封装一个高复用性、支持主题切换和自适应布局的ECharts组件,并探讨其核心实现原理与优化策略。
一、环境准备与基础实现
1.1 安装依赖
npm install echarts echarts-wordcloud echarts-gl
通过npm安装核心库及扩展插件,支持词云图、3D图表等高级功能。
1.2 组件核心结构
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'
import * as echarts from 'echarts'
import { useAppStore } from '@/store/modules/app'
const appStore = useAppStore()
const isDark = computed(() => appStore.getIsDark)
const theme = computed(() => isDark.value ? true : 'auto')
通过Vue3组合式API实现状态管理,结合Vuex存储用户主题偏好。
二、关键功能实现解析
2.1 响应式图表初始化
const elRef = ref<ElRef>()
const initChart = () => {
if (elRef.value) {
echartRef = echarts.init(elRef.value as HTMLElement)
echartRef?.setOption(options.value)
}
}
onMounted(() => initChart())
• 使用ref
创建DOM引用,确保在组件挂载后初始化实例
• 通过defineExpose
暴露getEchartsInstance
方法,支持外部调用
2.2 主题动态切换
const options = computed(() => ({
...props.options,
darkMode: theme.value
}))
• 通过计算属性动态合并主题配置,支持深色/浅色模式自动切换
• 结合Vuex实现主题状态持久化(需自行扩展)
2.3 自适应布局优化
const resizeHandler = debounce(() => {
echartRef?.resize()
}, 100)
onMounted(() => {
window.addEventListener('resize', resizeHandler)
observer.observe(elRef.value)
})
• 使用ResizeObserver
监听容器尺寸变化,提升响应速度
• 通过防抖函数避免高频触发导致的性能问题
三、高级功能扩展
3.1 动态数据更新
watch(options, (newOptions) => {
echartRef?.setOption(newOptions, true)
}, { deep: true })
• 通过深度监听实现数据动态更新
• 第二个参数true
表示合并更新而非替换
3.2 3D图表集成
echarts.use([GraphChart]) // 引入3D图表模块
• 支持Graph3D、Bar3D等类型,需在配置项中指定renderer: 'canvas'
3.3 交互增强
const option = {
tooltip: {
trigger: 'axis',
formatter: params => `${params[0].name}: ${params[0].value}`
},
toolbox: {
feature: {
saveAsImage: {}
}
}
}
• 自定义提示框格式
• 添加导出图片功能
四、性能优化策略
4.1 实例销毁
onBeforeUnmount(() => {
observer.disconnect()
window.removeEventListener('resize', resizeHandler)
echartRef?.dispose()
})
• 确保组件销毁时释放资源,防止内存泄漏
4.2 懒加载优化
const loadEcharts = async () => {
const module = await import('echarts')
echarts.use([GraphChart])
}
• 按需加载模块,减少首屏体积
五、典型配置项详解
配置项 | 类型 | 说明 |
---|---|---|
title | Object | 标题组件配置,支持主副标题、位置、样式等 |
tooltip | Object | 提示框配置,支持触发方式、格式化函数等 |
dataZoom | Object | 数据缩放配置,支持内部缩放与滑块缩放 |
toolbox | Object | 工具栏配置,支持数据视图、保存图片等功能 |
六、注意事项
- 容器元素需设置宽高,推荐使用百分比或CSS变量
- 避免在模板中直接写死样式,优先通过CSS类控制
- 复杂图表建议设置
animation: false
提升性能
完整代码
<script lang="ts" setup>
import type { EChartsOption } from 'echarts'
import echarts from '@/plugins/echarts'
import { debounce } from 'lodash-es'
import 'echarts-wordcloud'
import { propTypes } from '@/utils/propTypes'
import { PropType } from 'vue'
import { useAppStore } from '@/store/modules/app'
import { isString } from '@/utils/is'
import { useDesign } from '@/hooks/web/useDesign'
import 'echarts-gl'
import { GraphChart } from 'echarts/charts';
echarts.use([GraphChart]);
defineOptions({ name: 'EChart' })
const { getPrefixCls, variables } = useDesign()
const prefixCls = getPrefixCls('echart')
const appStore = useAppStore()
const props = defineProps({
options: {
type: Object as PropType<EChartsOption>,
required: true
},
width: propTypes.oneOfType([Number, String]).def(''),
height: propTypes.oneOfType([Number, String]).def('500px')
})
const isDark = computed(() => appStore.getIsDark)
const theme = computed(() => {
const echartTheme: boolean | string = unref(isDark) ? true : 'auto'
return echartTheme
})
const options = computed(() => {
return Object.assign(props.options, {
darkMode: unref(theme)
})
})
const elRef = ref<ElRef>()
const observer = new ResizeObserver((entries) => {
resizeHandler()
})
let echartRef: Nullable<echarts.ECharts> = null
const contentEl = ref<Element>()
const styles = computed(() => {
const width = isString(props.width) ? props.width : `${props.width}px`
const height = isString(props.height) ? props.height : `${props.height}px`
return {
width,
height
}
})
const initChart = () => {
if (unref(elRef) && props.options) {
echartRef = echarts.init(unref(elRef) as HTMLElement)
echartRef?.setOption(unref(options))
}
}
const getEchartsInstance = () => {
return echartRef
}
//导出 echartRef
defineExpose({ getEchartsInstance }) // 提供 open 方法,用于打开弹窗
watch(
() => options.value,
(options) => {
if (echartRef) {
echartRef?.setOption(options)
}
},
{
deep: true
}
)
const resizeHandler = debounce(() => {
if (echartRef) {
echartRef.resize()
}
}, 100)
const contentResizeHandler = async (e: TransitionEvent) => {
if (e.propertyName === 'width') {
resizeHandler()
}
}
onMounted(() => {
initChart()
window.addEventListener('resize', resizeHandler)
observer.observe(elRef.value)
contentEl.value = document.getElementsByClassName(`${variables.namespace}-layout-content`)[0]
unref(contentEl) &&
(unref(contentEl) as Element).addEventListener('transitionend', contentResizeHandler)
})
onBeforeUnmount(() => {
observer.disconnect()
window.removeEventListener('resize', resizeHandler)
unref(contentEl) &&
(unref(contentEl) as Element).removeEventListener('transitionend', contentResizeHandler)
})
onActivated(() => {
if (echartRef) {
echartRef.resize()
}
})
</script>
<template>
<div ref="elRef" :class="[$attrs.class, prefixCls]" :style="styles"></div>
</template>