0


深入浅出--vue3封装echarts组件

1、引言

在现代Web应用开发中,数据可视化已成为不可或缺的一部分。ECharts,作为一款强大的图表库,提供了丰富的图表类型和高度定制化的选项,深受开发者喜爱。然而,在Vue项目中直接使用ECharts可能会遇到状态管理、响应式更新和组件化封装的挑战。本文将介绍如何在Vue3中封装一个高效、可复用的ECharts组件——

  1. TChart

2、组件亮点

  • 响应式图表:自动调整大小以适应容器。
  • 空数据展示:支持自定义空数据状态显示。
  • 事件监听:自动绑定和解绑图表事件。
  • 主题切换:动态改变图表主题。
  • 性能优化:通过防抖函数减少不必要的渲染和资源消耗。

3、技术栈

  • Vue 3: 使用Composition API进行状态管理和逻辑组织。
  • ECharts: 数据可视化核心库。
  • VueUse: 提供useResizeObserver等实用工具函数。

4、组件结构

  1. TChart

组件的核心在于其模板和脚本部分:

  1. 模板:包含图表容器和空数据状态展示插槽。
  2. 脚本
    • 初始化图表并设置选项。- 监听窗口和图表容器尺寸变化,实现响应式布局。- 自动绑定和解绑图表事件。- 支持动态主题切换和选项更新。

5、实现步骤

5.1 安装echarts

  1. npm install echarts

5.2 注册echarts

并在 main 文件中注册使用

  1. import * as echarts from "echarts" // 引入echarts
  2. app.config.globalProperties.$echarts = echarts // 全局使用

5.3 新建TChart组件

~components/TCharts.vue

  1. <template>
  2. <div class="t-chart" v-bind="$attrs">
  3. <div
  4. v-show="!formatEmpty"
  5. class="t-chart-container"
  6. :id="id"
  7. ref="echartRef"
  8. />
  9. <slot v-if="formatEmpty" name="empty">
  10. <el-empty v-bind="$attrs" :description="description" />
  11. </slot>
  12. <slot></slot>
  13. </div>
  14. </template>
  15. <script setup lang="ts" name="TChart">
  16. import {
  17. onMounted,
  18. getCurrentInstance,
  19. ref,
  20. watch,
  21. nextTick,
  22. onBeforeUnmount,
  23. markRaw,
  24. useAttrs,
  25. } from 'vue'
  26. import { useResizeObserver } from '@vueuse/core'
  27. import { debounce, toLine } from '../../utils'
  28. import { computed } from 'vue'
  29. const { proxy } = getCurrentInstance() as any
  30. const props = defineProps({
  31. options: {
  32. type: Object,
  33. default: () => ({}),
  34. },
  35. id: {
  36. type: String,
  37. default: () => Math.random().toString(36).substring(2, 8),
  38. },
  39. theme: {
  40. type: String,
  41. default: '',
  42. },
  43. isEmpty: {
  44. type: [Boolean, Function],
  45. default: false,
  46. },
  47. description: {
  48. type: String,
  49. default: '暂无数据',
  50. },
  51. })
  52. const echartRef = ref<HTMLDivElement>()
  53. const chart = ref()
  54. const emits = defineEmits()
  55. const events = Object.entries(useAttrs())
  56. // 图表初始化
  57. const renderChart = () => {
  58. chart.value = markRaw(proxy.$echarts.init(echartRef.value, props.theme))
  59. setOption(props.options)
  60. // 返回chart实例
  61. emits('chart', chart.value)
  62. // 监听图表事件
  63. events.forEach(([key, value]) => {
  64. if (key.startsWith('on') && !key.startsWith('onChart')) {
  65. const on = toLine(key).substring(3)
  66. chart.value.on(on, (...args) => emits(on, ...args))
  67. }
  68. })
  69. // 监听元素变化
  70. useResizeObserver(echartRef.value, resizeChart)
  71. // 如果不想用vueuse,可以使用下边的方法代替,但组件使用v-show时,不会触发resize事件
  72. // window.addEventListener('resize', resizeChart)
  73. }
  74. // 重绘图表函数
  75. const resizeChart = debounce(
  76. () => {
  77. chart.value?.resize()
  78. },
  79. 300,
  80. true
  81. )
  82. // 设置图表函数
  83. const setOption = debounce(
  84. async (data) => {
  85. if (!chart.value) return
  86. chart.value.setOption(data, true, true)
  87. await nextTick()
  88. resizeChart()
  89. },
  90. 300,
  91. true
  92. )
  93. const formatEmpty = computed(() => {
  94. if (typeof props.isEmpty === 'function') {
  95. return props.isEmpty(props.options)
  96. }
  97. return props.isEmpty
  98. })
  99. watch(
  100. () => props.options,
  101. async (nw) => {
  102. await nextTick()
  103. setOption(nw)
  104. },
  105. { deep: true }
  106. )
  107. watch(
  108. () => props.theme,
  109. async () => {
  110. chart.value.dispose()
  111. renderChart()
  112. }
  113. )
  114. onMounted(() => {
  115. renderChart()
  116. })
  117. onBeforeUnmount(() => {
  118. // 取消监听
  119. // window.removeEventListener('resize', resizeChart)
  120. // 销毁echarts实例
  121. chart.value.dispose()
  122. chart.value = null
  123. })
  124. </script>
  125. <style lang="scss" scoped>
  126. .t-chart {
  127. position: relative;
  128. width: 100%;
  129. height: 100%;
  130. &-container {
  131. width: 100%;
  132. height: 100%;
  133. }
  134. }
  135. </style>

utils/index.ts

  1. type Func = (...args: any[]) => any
  2. /**
  3. * 防抖函数
  4. * @param { Function } func 函数
  5. * @param { Number } delay 防抖时间
  6. * @param { Boolean } immediate 是否立即执行
  7. * @param { Function } resultCallback
  8. */
  9. export function debounce(
  10. func: Func,
  11. delay: number = 500,
  12. immediate?: boolean,
  13. resultCallback?: Func
  14. ) {
  15. let timer: null | ReturnType<typeof setTimeout> = null
  16. let isInvoke = false
  17. const _debounce = function (this: unknown, ...args: any[]) {
  18. return new Promise((resolve, reject) => {
  19. if (timer) clearTimeout(timer)
  20. if (immediate && !isInvoke) {
  21. try {
  22. const result = func.apply(this, args)
  23. if (resultCallback) resultCallback(result)
  24. resolve(result)
  25. } catch (e) {
  26. reject(e)
  27. }
  28. isInvoke = true
  29. } else {
  30. timer = setTimeout(() => {
  31. try {
  32. const result = func.apply(this, args)
  33. if (resultCallback) resultCallback(result)
  34. resolve(result)
  35. } catch (e) {
  36. reject(e)
  37. }
  38. isInvoke = false
  39. timer = null
  40. }, delay)
  41. }
  42. })
  43. }
  44. _debounce.cancel = function () {
  45. if (timer) clearTimeout(timer)
  46. isInvoke = false
  47. timer = null
  48. }
  49. return _debounce
  50. }
  51. /**
  52. * 节流函数
  53. * @param { Function } func
  54. * @param { Boolean } interval
  55. * @param { Object } options
  56. * leading:初始 trailing:结尾
  57. */
  58. export function throttle(
  59. func: Func,
  60. interval: number,
  61. options = { leading: false, trailing: true }
  62. ) {
  63. let timer: null | ReturnType<typeof setTimeout> = null
  64. let lastTime = 0
  65. const { leading, trailing } = options
  66. const _throttle = function (this: unknown, ...args: any[]) {
  67. const nowTime = Date.now()
  68. if (!lastTime && !leading) lastTime = nowTime
  69. const remainTime = interval - (nowTime - lastTime)
  70. if (remainTime <= 0) {
  71. if (timer) {
  72. clearTimeout(timer)
  73. timer = null
  74. }
  75. lastTime = nowTime
  76. func.apply(this, args)
  77. }
  78. if (trailing && !timer) {
  79. timer = setTimeout(() => {
  80. lastTime = !leading ? 0 : Date.now()
  81. timer = null
  82. func.apply(this, args)
  83. }, remainTime)
  84. }
  85. }
  86. _throttle.cancel = function () {
  87. if (timer) clearTimeout(timer)
  88. timer = null
  89. lastTime = 0
  90. }
  91. return _throttle
  92. }
  93. /**
  94. * 驼峰转换下划线
  95. * @param { String } name
  96. */
  97. export function toLine(name: string) {
  98. return name.replace(/([A-Z])/g, '_$1').toLowerCase()
  99. }

6、使用组件

7.1使用示例

  1. <template>
  2. <div>
  3. <el-button @click="isShow = !isShow">{{
  4. isShow ? '隐藏' : '显示'
  5. }}</el-button>
  6. <el-button @click="addData()">增加数据</el-button>
  7. <t-chart
  8. v-show="isShow"
  9. :options="options"
  10. style="width: 100%; height: 500px"
  11. @click="click"
  12. @dblclick="addData()"
  13. @mousedown="mousedown"
  14. @mousemove="mousemove"
  15. @mouseover="mouseover"
  16. @mouseout="mouseout"
  17. @globalout="globalout"
  18. @contextmenu="contextmenu"
  19. @chart="chart"
  20. />
  21. </div>
  22. </template>
  23. <script setup lang="ts">
  24. import TChart from '../components/TChart'
  25. import { ref } from 'vue'
  26. const options = ref({
  27. xAxis: {
  28. type: 'category',
  29. data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
  30. },
  31. yAxis: {
  32. type: 'value',
  33. },
  34. series: [
  35. {
  36. data: [150, 230, 224, 218, 135, 147, 260],
  37. type: 'line',
  38. },
  39. ],
  40. })
  41. const isShow = ref(true)
  42. const addData = () => {
  43. options.value.xAxis.data.push(
  44. 'test' + Math.random().toString(36).substring(2, 8)
  45. )
  46. options.value.series[0].data.push(Math.random() * 200)
  47. }
  48. const click = (e) => {
  49. console.log('click-----', e)
  50. }
  51. const mousedown = (e) => {
  52. console.log('mousedown-----', e)
  53. }
  54. const mousemove = (e) => {
  55. console.log('mousemove-----', e)
  56. }
  57. const mouseover = (e) => {
  58. console.log('mouseover-----', e)
  59. }
  60. const mouseout = (e) => {
  61. console.log('mouseout-----', e)
  62. }
  63. const globalout = (e) => {
  64. console.log('globalout-----', e)
  65. }
  66. const contextmenu = (e) => {
  67. console.log('contextmenu-----', e)
  68. }
  69. const chartRef = ref(null)
  70. const chart = (dom) => {
  71. chartRef.value = dom
  72. }
  73. </script>

7.2效果图

更多使用示例,可以看看文档 TChart 图表

深入浅出---封装思路

接下来,我想和大家聊一下封装思路。首先,封装一个组件,我需要考虑什么,从哪方面考虑,让我们看看ai的回答

封装组件是前端开发中的一种常见实践,它有助于提高代码的复用性、可维护性和可读性。在进行组件封装时,应遵循以下原则:

  1. 单一职责原则:一个组件应该只负责一项功能,这样可以保证组件的简洁和可复用性。
  2. 高内聚低耦合:组件内部的功能应该紧密相关(高内聚),而组件之间的依赖关系应该尽量减少(低耦合)。这有助于提高组件的独立性和灵活性。
  3. 明确的输入输出:组件应该有清晰的输入(props)和输出(state变化或事件)。这有助于其他开发者理解组件的使用方式。
  4. 可复用性:设计组件时应考虑到其复用性,避免硬编码特定场景的逻辑,使得组件可以在多种场景下使用。
  5. 可测试性:组件应当易于测试,可以通过单元测试和集成测试验证其功能。避免在组件中使用过多的全局状态或复杂的异步操作,以降低测试难度。
  6. 响应式设计:组件应该能够根据屏幕尺寸和其他环境因素自动调整布局和样式,提供良好的用户体验。
  7. 文档和注释:为组件编写详细的文档和注释,说明组件的用途、输入参数、返回值和使用示例,帮助其他开发者快速理解和使用组件。
  8. 性能优化:在可能的情况下,对组件进行性能优化,如使用虚拟DOM、避免不必要的重渲染等,提高应用的运行效率。
  9. 一致性:保持组件风格和行为的一致性,遵循项目的编码规范和设计指南,提高整体的代码质量和用户体验。
  10. 模块化:将大型组件分解成多个小的、可组合的子组件,每个子组件负责一部分功能,通过组合这些子组件来构建更复杂的组件。

遵循这些原则可以帮助你创建出高质量、可维护的组件,从而提升整个项目的开发效率和代码质量。

基于上边的原则,我们再考虑下封装echarts的实现。

接下来,我们来看一下官方的示例

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>ECharts</title>
  6. <!-- 引入刚刚下载的 ECharts 文件 -->
  7. <script src="echarts.js"></script>
  8. </head>
  9. <body>
  10. <!-- 为 ECharts 准备一个定义了宽高的 DOM -->
  11. <div id="main" style="width: 600px;height:400px;"></div>
  12. <script type="text/javascript">
  13. // 基于准备好的dom,初始化echarts实例
  14. var myChart = echarts.init(document.getElementById('main'));
  15. // 指定图表的配置项和数据
  16. var option = {
  17. title: {
  18. text: 'ECharts 入门示例'
  19. },
  20. tooltip: {},
  21. legend: {
  22. data: ['销量']
  23. },
  24. xAxis: {
  25. data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
  26. },
  27. yAxis: {},
  28. series: [
  29. {
  30. name: '销量',
  31. type: 'bar',
  32. data: [5, 20, 36, 10, 10, 20]
  33. }
  34. ]
  35. };
  36. // 使用刚指定的配置项和数据显示图表。
  37. myChart.setOption(option);
  38. </script>
  39. </body>
  40. </html>

实现的步骤的步骤有哪些?

  1. 引入echarts
  2. 定义一个DOM元素(容器)
  3. 获取DOM元素(容器)并初始化echarts实例
  4. 指定图表的配置项和数据
  5. 使用刚指定的配置项和数据显示图表。

每当我想使用echarts组件时,都得经过这五个步骤。当我想实现多个图表时,这多个图表对比起来,哪些是步骤是变化的?哪些的不变的?

细心的网友会发现,其中,变化最多的,是第四个步骤“图表的配置项和数据”。那我,是不是可以将这些重复性的操作,封装到组件里,让组件替我去完成。

接下来,让我们来一步一步实现代码

1.基本功能

1.1准备DOM元素(容器)
  1. <template>
  2. <div class="t-chart" v-bind="$attrs">
  3. <div v-show="!formatEmpty" class="t-chart" :id="id" ref="echartRef" />
  4. </template>
  5. <style lang="scss" scoped>
  6. .t-chart {
  7. width: 100%;
  8. height: 100%;
  9. }
  10. </style>
2.2 获取容器并初始化echarts实例

优化小技巧:通过ref获取dom实例比document操作获取dom,性能更好

  1. <template>
  2. <div class="t-chart" v-bind="$attrs">
  3. <div v-show="!formatEmpty" class="t-chart" :id="id" ref="echartRef" />
  4. </template>
  5. <script setup lang="ts" name="TChart">
  6. import { onMounted, getCurrentInstance, ref, markRaw } from "vue"
  7. const { proxy } = getCurrentInstance() as any
  8. const props = defineProps({
  9. options: {
  10. type: Object,
  11. default: () => ({})
  12. },
  13. id: {
  14. type: String,
  15. default: () => Math.random().toString(36).substring(2, 8)
  16. }
  17. })
  18. const echartRef = ref<HTMLDivElement>()
  19. const chart = ref()
  20. // 图表初始化
  21. const renderChart = () => {
  22. chart.value = markRaw(proxy.$echarts.init(echartRef.value))
  23. }
  24. onMounted(() => {
  25. renderChart()
  26. })
  27. </script>
  28. <style lang="scss" scoped>
  29. .t-chart {
  30. width: 100%;
  31. height: 100%;
  32. }
  33. </style>
1.3 设置配置项和数据
  1. <template>
  2. <div class="t-chart" v-bind="$attrs">
  3. <div v-show="!formatEmpty" class="t-chart" :id="id" ref="echartRef" />
  4. </template>
  5. <script setup lang="ts" name="TChart">
  6. import { onMounted, getCurrentInstance, ref, markRaw } from "vue"
  7. const { proxy } = getCurrentInstance() as any
  8. const props = defineProps({
  9. options: {
  10. type: Object,
  11. default: () => ({})
  12. },
  13. id: {
  14. type: String,
  15. default: () => Math.random().toString(36).substring(2, 8)
  16. }
  17. })
  18. const echartRef = ref<HTMLDivElement>()
  19. const chart = ref()
  20. // 图表初始化
  21. const renderChart = () => {
  22. chart.value = markRaw(proxy.$echarts.init(echartRef.value))
  23. setOption(props.options)
  24. }
  25. // 设置图表函数
  26. const setOption = data => {
  27. chart.value.setOption(data, true, true)
  28. chart.value?.resize()
  29. }
  30. onMounted(() => {
  31. renderChart()
  32. })
  33. </script>
  34. <style lang="scss" scoped>
  35. .t-chart {
  36. width: 100%;
  37. height: 100%;
  38. }
  39. </style>

2.组件要实现的功能

很多时候,封装封装组件,并不是一次性就能做到很完美的状态,而是在使用中, 不断去优化,取改进的。比如,在使用中,数据更新、页面大小变化时,图表没有重新渲染、echart事件没有触发。这些都是一点点去优化改进的。记住一个准则:“先实现再优化”

  • 响应式图表
  • 图表尺寸的自适应
  • 事件监听
  • 性能优化
  • 空数据展示
  • 插槽
  • 主题切换
  • 获取echarts实例

3.响应式图表

希望数据变化时,可以重新绘制图表

  1. // 重绘图表函数
  2. const resizeChart = debounce(
  3. () => {
  4. chart.value?.resize()
  5. },
  6. 300,
  7. true
  8. )
  9. // 设置图表函数
  10. const setOption = debounce(
  11. async data => {
  12. if (!chart.value) return
  13. chart.value.setOption(data, true, true)
  14. await nextTick()
  15. resizeChart()
  16. },
  17. 300,
  18. true
  19. )
  20. const formatEmpty = computed(() => {
  21. if (typeof props.isEmpty === "function") {
  22. return props.isEmpty(props.options)
  23. }
  24. return props.isEmpty
  25. })
  26. // 监听数据变化时,重绘
  27. watch(
  28. () => props.options,
  29. async nw => {
  30. await nextTick()
  31. setOption(nw)
  32. },
  33. { deep: true }
  34. )

4.图表尺寸的自适应

希望容器尺寸变化时,图表能够自适应

笔者这边使用了vueuse的useResizeObserver,来实现对元素变化的监听,为什么没用resize? 是因为其中有坑。

1、window大小变化时,才会触发监听

2、使用组件使用v-show的时候,不会触发,可能会蜷缩在一团

  1. import { useResizeObserver } from "@vueuse/core"
  2. const renderChart = () => {
  3. chart.value = markRaw(proxy.$echarts.init(echartRef.value, props.theme))
  4. setOption(props.options)
  5. // 监听元素变化
  6. useResizeObserver(echartRef.value, resizeChart)
  7. // 大小自适应
  8. // window.addEventListener('resize', resizeChart)
  9. }
  10. onBeforeUnmount(() => {
  11. // 取消监听
  12. // window.removeEventListener('resize', resizeChart)
  13. })

5.事件监听

通过useAttrs,拿到父组件传过来的事件,并批量注册emits事件

  1. const events = Object.entries(useAttrs())
  2. // 监听图表事件
  3. events.forEach(([key, value]) => {
  4. if (key.startsWith('on') && !key.startsWith('onChart')) {
  5. const on = toLine(key).substring(3)
  6. chart.value.on(on, (...args) => emits(on, ...args))
  7. }
  8. })

6.性能优化

  • 通过markRaw,将echarts实例标记为普通对象,减少响应式带来的损耗。
  • 防抖函数,用于图表重绘和选项更新,减少不必要的调用,提高性能。
  • 当组件被销毁时,调用 dispose 方法销毁实例,防止可能的内存泄漏。
  1. chart.value = markRaw(proxy.$echarts.init(echartRef.value, props.theme))
  2. // 重绘图表函数
  3. const resizeChart = debounce(
  4. () => {
  5. chart.value?.resize()
  6. },
  7. 300,
  8. true
  9. )
  10. // 设置图表函数
  11. const setOption = debounce(
  12. async data => {
  13. if (!chart.value) return
  14. chart.value.setOption(data, true, true)
  15. await nextTick()
  16. resizeChart()
  17. },
  18. 300,
  19. true
  20. )
  21. onBeforeUnmount(() => {
  22. // 销毁echarts实例
  23. chart.value.dispose()
  24. chart.value = null
  25. })

6.空数据展示

组件可以通过isEmpty,来设置echarts图表空状态,类型可以是Boolean,也可以是个函数,方便灵活调用,还可以设置description,空数据时的展示文字

  1. <template>
  2. <div class="t-chart" v-bind="$attrs">
  3. <div
  4. v-show="!formatEmpty"
  5. class="t-chart-container"
  6. :id="id"
  7. ref="echartRef"
  8. />
  9. <slot v-if="formatEmpty" name="empty">
  10. <el-empty v-bind="$attrs" :description="description" />
  11. </slot>
  12. <slot></slot>
  13. </div>
  14. </template>
  15. <script setup lang="ts" name="TChart">
  16. const props = defineProps({
  17. isEmpty: {
  18. type: [Boolean, Function],
  19. default: false,
  20. },
  21. description: {
  22. type: String,
  23. default: '暂无数据',
  24. },
  25. })
  26. const formatEmpty = computed(() => {
  27. if (typeof props.isEmpty === 'function') {
  28. return props.isEmpty(props.options)
  29. }
  30. return props.isEmpty
  31. })
  32. ...
  33. </script>

7.插槽

可以通过插槽,在组件内增加内容,也可以替换空状态的内容

  1. <template>
  2. <div class="t-chart" v-bind="$attrs">
  3. <div
  4. v-show="!formatEmpty"
  5. class="t-chart-container"
  6. :id="id"
  7. ref="echartRef"
  8. />
  9. <slot v-if="formatEmpty" name="empty">
  10. <el-empty v-bind="$attrs" :description="description" />
  11. </slot>
  12. <slot></slot>
  13. </div>
  14. </template>
  15. <style lang="scss" scoped>
  16. .t-chart {
  17. position: relative;
  18. width: 100%;
  19. height: 100%;
  20. &-container {
  21. width: 100%;
  22. height: 100%;
  23. }
  24. }
  25. </style>

8.主题切换

监听props的主题,动态切换echarts 主题

  1. const props = defineProps({
  2. theme: {
  3. type: String,
  4. default: '',
  5. }
  6. })
  7. // 图表初始化
  8. const renderChart = () => {
  9. chart.value = markRaw(proxy.$echarts.init(echartRef.value, props.theme))
  10. // ...
  11. }
  12. watch(
  13. () => props.theme,
  14. async () => {
  15. chart.value.dispose()
  16. renderChart()
  17. }
  18. )

9.获取echarts实例

注册了echarts实例后,将实例返回给父组件

  1. chart.value = markRaw(proxy.$echarts.init(echartRef.value, props.theme))
  2. // 返回chart实例
  3. emits('chart', chart.value)

完整代码

具体的,可以回看5.3 新建TChart组件

  1. <template>
  2. <div class="t-chart" v-bind="$attrs">
  3. <div
  4. v-show="!formatEmpty"
  5. class="t-chart-container"
  6. :id="id"
  7. ref="echartRef"
  8. />
  9. <slot v-if="formatEmpty" name="empty">
  10. <el-empty v-bind="$attrs" :description="description" />
  11. </slot>
  12. <slot></slot>
  13. </div>
  14. </template>
  15. <script setup lang="ts" name="TChart">
  16. import {
  17. onMounted,
  18. getCurrentInstance,
  19. ref,
  20. watch,
  21. nextTick,
  22. onBeforeUnmount,
  23. markRaw,
  24. useAttrs,
  25. } from 'vue'
  26. import { useResizeObserver } from '@vueuse/core'
  27. import { debounce, toLine } from '../../utils'
  28. import { computed } from 'vue'
  29. const { proxy } = getCurrentInstance() as any
  30. const props = defineProps({
  31. options: {
  32. type: Object,
  33. default: () => ({}),
  34. },
  35. id: {
  36. type: String,
  37. default: () => Math.random().toString(36).substring(2, 8),
  38. },
  39. theme: {
  40. type: String,
  41. default: '',
  42. },
  43. isEmpty: {
  44. type: [Boolean, Function],
  45. default: false,
  46. },
  47. description: {
  48. type: String,
  49. default: '暂无数据',
  50. },
  51. })
  52. const echartRef = ref<HTMLDivElement>()
  53. const chart = ref()
  54. const emits = defineEmits()
  55. const events = Object.entries(useAttrs())
  56. // 图表初始化
  57. const renderChart = () => {
  58. chart.value = markRaw(proxy.$echarts.init(echartRef.value, props.theme))
  59. setOption(props.options)
  60. // 返回chart实例
  61. emits('chart', chart.value)
  62. // 监听图表事件
  63. events.forEach(([key, value]) => {
  64. if (key.startsWith('on') && !key.startsWith('onChart')) {
  65. const on = toLine(key).substring(3)
  66. chart.value.on(on, (...args) => emits(on, ...args))
  67. }
  68. })
  69. // 监听元素变化
  70. useResizeObserver(echartRef.value, resizeChart)
  71. // 大小自适应
  72. // window.addEventListener('resize', resizeChart)
  73. }
  74. // 重绘图表函数
  75. const resizeChart = debounce(
  76. () => {
  77. chart.value?.resize()
  78. },
  79. 300,
  80. true
  81. )
  82. // 设置图表函数
  83. const setOption = debounce(
  84. async (data) => {
  85. if (!chart.value) return
  86. chart.value.setOption(data, true, true)
  87. await nextTick()
  88. resizeChart()
  89. },
  90. 300,
  91. true
  92. )
  93. const formatEmpty = computed(() => {
  94. if (typeof props.isEmpty === 'function') {
  95. return props.isEmpty(props.options)
  96. }
  97. return props.isEmpty
  98. })
  99. watch(
  100. () => props.options,
  101. async (nw) => {
  102. await nextTick()
  103. setOption(nw)
  104. },
  105. { deep: true }
  106. )
  107. watch(
  108. () => props.theme,
  109. async () => {
  110. chart.value.dispose()
  111. renderChart()
  112. }
  113. )
  114. onMounted(() => {
  115. renderChart()
  116. })
  117. onBeforeUnmount(() => {
  118. // 取消监听
  119. // window.removeEventListener('resize', resizeChart)
  120. // 销毁echarts实例
  121. chart.value.dispose()
  122. chart.value = null
  123. })
  124. </script>
  125. <style lang="scss" scoped>
  126. .t-chart {
  127. position: relative;
  128. width: 100%;
  129. height: 100%;
  130. &-container {
  131. width: 100%;
  132. height: 100%;
  133. }
  134. }
  135. </style>

最后看看是否符合组件的设计原则

以上,就是我实现echarts组件的思路。希望对您有帮助

标签: vue.js echarts 前端

本文转载自: https://blog.csdn.net/Ja_dream/article/details/140481269
版权归原作者 Ja_dream 所有, 如有侵权,请联系我们删除。

“深入浅出--vue3封装echarts组件”的评论:

还没有评论