Vue3中封装和使用ECharts

背景

近期在做数据大屏项目,使用到了ECharts图表,一段时间没用又要去翻文档了,好在现在有了通义灵码Kimi.ai的辅助,一些小的配置项只用问这些AI即可,大部分都能够答对。但是有些配置项在新版本ECharts里做了更改,比如normal属性,需要注意鉴别。这里顺便再推荐下大屏适配的库:autofit

对于Vue3,直接挂载Echarts到vue实例上不是个好选择。因为与<script setup> 搭配使用组合式API时没有this 共开发者调用,需要先获取当前实例再调用,如下方式:

const { proxy } = getCurrentInstance()

const options = {}
proxy.$echarts.init(options)

这里我使用了组合式API去封装ECharts,然后在各图标组件中调用。

以下是具体做法:

封装

// hooks/useECharts.js

import * as echarts from 'echarts/core'

// 按需引入所需图表
import { 
  BarChart, 
  EffectScatterChart,
  LineChart, 
  LinesChart, 
  MapChart, 
  PieChart 
} from 'echarts/charts'

// 引入图表所需组件
import {
  DatasetComponent,
  GeoComponent,
  GraphicComponent,
  GridComponent,
  LegendComponent,
  TitleComponent,
  TooltipComponent,
  TransformComponent,
} from 'echarts/components'

import { LabelLayout, UniversalTransition } from 'echarts/features'

import { CanvasRenderer } from 'echarts/renderers'

echarts.use([
  TitleComponent,
  TooltipComponent,
  GridComponent,
  DatasetComponent,
  TransformComponent,
  GeoComponent,
  LabelLayout,
  UniversalTransition,
  CanvasRenderer,
  MapChart,
  BarChart,
  LinesChart,
  LineChart,
  PieChart,
  EffectScatterChart,
  LegendComponent,
  GraphicComponent,
])

export default function () {
  const chartElement = shallowRef()
  const chartInstance = shallowRef()

  // 尺寸变化
  function resize() {
    chartInstance.value?.resize()
  }

  // 注册地图
  function registerMap(name: string, json: any) {
    echarts.registerMap(name, json)
  }

  // 图表绘制
  function render() {
    chartInstance.value = echarts.init(chartElement.value)
  }

  // 图表销毁
  function destroy() {
    chartInstance.value?.dispose()
    chartInstance.value = null
  }

  onMounted(() => {
    render()
    window.addEventListener('resize', resize)
  })

  onBeforeUnmount(() => {
    destroy()
    window.removeEventListener('resize', resize)
  })

  return {
    registerMap,
    chartInstance,
    chartElement,
  }
}

使用

<script setup>
import useCharts from '@/hooks/useEcharts'
const { chartInstance, chartElement } = useCharts()

const options = {
  // ...
}

onMounted(() => {
  if (chartInstance.value)
    chartInstance.value.setOption(options)
})
</script>

<template>
  <div class="view-container">
    <div ref="chartElement" class="line-chart" />
  </div>
</template>

其他方法

以上是我目前使用ECharts的方法,最近看到个开源的后台项目:nova-admin,里面也封装了ECharts,这里也放下这个项目的封装方法:

import * as echarts from 'echarts/core'
import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts'

// 系列类型的定义后缀都为 SeriesOption
import type {
  BarSeriesOption,
  LineSeriesOption,
  PieSeriesOption,
  RadarSeriesOption,
} from 'echarts/charts'

// 组件类型的定义后缀都为 ComponentOption
import type {
  DatasetComponentOption,
  GridComponentOption,
  LegendComponentOption,
  TitleComponentOption,
  ToolboxComponentOption,
  TooltipComponentOption,
} from 'echarts/components'
import {
  DatasetComponent, // 数据集组件
  GridComponent,
  LegendComponent,
  TitleComponent,
  ToolboxComponent,
  TooltipComponent,
  TransformComponent, // 内置数据转换器组件 (filter, sort)
} from 'echarts/components'

import { LabelLayout, UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
import { useAppStore } from '@/store'

// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
export type ECOption = echarts.ComposeOption<
  | BarSeriesOption
  | PieSeriesOption
  | LineSeriesOption
  | TitleComponentOption
  | TooltipComponentOption
  | GridComponentOption
  | LegendComponentOption
  | DatasetComponentOption
  | ToolboxComponentOption
  | RadarSeriesOption
>

// 注册必须的组件
echarts.use([
  TitleComponent,
  TooltipComponent,
  GridComponent,
  LegendComponent,
  DatasetComponent,
  TransformComponent,
  BarChart,
  PieChart,
  LineChart,
  LabelLayout,
  UniversalTransition,
  CanvasRenderer,
  ToolboxComponent,
  RadarChart,
])

/**
 * Echarts hooks函数
 * @param options - 图表配置
 * @description 按需引入图表组件,没注册的组件需要先引入
 */
export function useEcharts(options: Ref<ECOption>) {
  const appStore = useAppStore()

  const domRef = ref<HTMLElement>()

  let chart: echarts.ECharts | null = null

  const initialSize = { width: 0, height: 0 }
  // useElementSize是@vueuse/core库中的,由于项目设置了自动导入,所以上方没有引入
  const { width, height } = useElementSize(domRef, initialSize)

  function canRender() {
    return initialSize.width > 0 && initialSize.height > 0
  }

  function isRendered() {
    return Boolean(domRef.value && chart)
  }
  async function render() {
    const chartTheme = appStore.colorMode ? 'dark' : 'light'
    await nextTick()
    if (domRef.value) {
      chart = echarts.init(domRef.value, chartTheme)
      update(options.value)
    }
  }

  function update(updateOptions: ECOption) {
    if (isRendered())
      chart!.setOption({ ...updateOptions, backgroundColor: 'transparent' })
  }

  function resize() {
    chart?.resize()
  }

  function destroy() {
    chart?.dispose()
    chart = null
  }
  const sizeWatch = watch([width, height], async ([newWidth, newHeight]) => {
    initialSize.width = newWidth
    initialSize.height = newHeight
    if (newWidth === 0 && newHeight === 0) {
      // 节点被删除 将chart置为空
      chart = null
    }
    if (!canRender())
      return
    if (isRendered())
      resize()
    else await render()
  })

  const OptionWatch = watch(options, (newValue) => {
    update(newValue)
  })

  onUnmounted(() => {
    sizeWatch()
    OptionWatch()
    destroy()
  })

  return {
    domRef,
  }
}

使用起来无需手动调用init了,只需传配置项即可:

<script setup lang="ts">
import { type ECOption, useEcharts } from '@/hooks'

const option = ref<ECOption>({
  //...
}) as Ref<ECOption>
const { domRef: lineRef } = useEcharts(option)
</script>

<template>
  <div ref="lineRef" />
</template>

这里使用是比我用的方法更加方便的,但是需要注册地图registerMap 的话就要另外加逻辑了,也没有导出EChart实例,有些效果如dispatchAction 手动高亮不好实现。简单的图表展示需求,还是可以考虑这种方法。

评论