切换语言为:繁体
在 Vue3 项目中封装 ECharts 组件技巧和思路

在 Vue3 项目中封装 ECharts 组件技巧和思路

  • 爱糖宝
  • 2024-06-13
  • 2171
  • 0
  • 0

目录

  • 一、相关知识

    • 1.1 什么是ECharts

    • 1.2 什么是组件

  • 二、为什么要封装ECharts组件

  • 三、如何在Vue3项目中封装ECharts组件

    • 3.1 ECharts的全量引入和按需引入

    • 3.2 支持数据响应式更新

    • 3.3 简化ECharts的配置工作

    • 3.4 支持自适应窗口大小

  • 四、进一步提升组件的实用性和便捷性

    • 4.1 巧用dataset

    • 4.2 结合axios请求进一步封装

  • 五、总结与后期改进

一、相关知识

1.1 什么是ECharts

ECharts是一个基于JavaScript的开源可视化库,以数据驱动、直观、交互丰富、可高度个性化定制为特点。它提供了丰富多样的图表类型和交互功能,可以帮助开发人员快速创建各种数据可视化图表,如折线图、柱状图、饼图、地图等。同时,ECharts还提供了多种数据交互和动画效果,使得数据可视化更加生动和有趣。

ECharts具有良好的兼容性和扩展性,兼容当前绝大部分浏览器(IE8/9/10/11,Chrome,Firefox,Safari等),支持移动端和PC端展示,同时提供了丰富的配置项和API,使用户能够灵活地定制和调整图表样式和行为。

由于其功能强大、易于上手和社区支持良好,ECharts已成为前端开发中常用的数据可视化工具之一。

1.2 什么是组件

组件是前端开发中一种模块化的设计方式,用于将特定功能、结构和样式封装成独立的单元。通过组件化的设计,开发人员可以将复杂的界面拆分为多个独立、可复用的部分,使代码更加清晰、可维护性更强;通过组合不同的组件,可以构建出丰富多样的用户界面。

在现代前端开发中,组件化已成为一种重要的开发模式,通过前端组件化能够提高团队协作效率,加快项目开发速度,便于后期进行功能的扩展和修改。

二、为什么要封装ECharts组件

数据可视化图表是前端开发中非常常见的功能需求,尤其在大屏和数据管理系统的开发中占有很高的比例,因此ECharts成了我们前端工程师经常使用的一个工具库。

在实际开发中,当项目中需要使用ECharts进行可视化图表的开发时,通常我们会直接参照官网提供的样例配置来生成所需的图表,类似这样:

// 绘制图表
myChart.setOption({
  title: {
    text: 'ECharts 入门示例'
  },
  tooltip: {},
  xAxis: {
    data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
  },
  yAxis: {},
  series: [
    {
      name: '销量',
      type: 'bar',
      data: [5, 20, 36, 10, 10, 20]
    }
  ]
});


以上只是一个柱状图的配置内容,而每个图表都得有一份自己的配置,这就导致了一个问题:随着项目中需要绘制的图表数量增加,配置变得越来越复杂,当页面中需要显示十几个ECharts图时,我们需要写十几份独立的配置。

这些配置不仅冗长还复杂,严重降低了代码的可读性和可维护性,在接入后端接口时也会带来一些麻烦;而且在同一个项目中我们用到的大部分ECharts配置都是相似的,每个图表的配置都有很多重复的内容,当需要统一修改某个配置项的内容时重复的工作量很大。在这种情况下,封装通用的ECharts组件成了一个更好的选择。

封装 Echarts 组件有以下几个好处:

  1. 简化使用:封装后的组件可以提供更简洁、更易用的 API,我们能够更快速地完成页面中 ECharts图表的绘制。

  2. 隐藏实现细节:封装可以隐藏 ECharts的具体实现细节,让我们不必过多关心图表配置项和底层实现,只需关注如何处理数据来进行展示。

  3. 提高复用性:封装后的组件可以被多个页面或项目共享使用,提高了代码的复用性和可维护性。

  4. 增强扩展性:封装可以在原有基础上进行功能扩展,比如添加自定义交互、动画效果等,从而满足更多定制化的需求。

  5. 提高可维护性:封装可以将相关的代码逻辑集中在一个组件中,便于维护和管理,减少代码冗余和维护成本。

通过封装 ECharts组件,我们可以提高开发效率,降低代码维护成本,并使项目变得更加模块化和可扩展化。

三、如何在Vue3项目中封装ECharts组件

3.1 ECharts的全量引入和按需引入

我们以ECharts 5.3.3版本为例,要在Vue3项目中使用ECharts,得安装echarts 依赖包,如果要使用3D图表功能,还得额外安装echarts-gl依赖:

npm install echarts echarts-gl --save

在使用ECharts时,我们可以选择全量引入或按需引入ECharts资源。

全量引入会导入ECharts中的所有图表和组件,使用起来比较便捷:

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

<script setup>
import * as echarts from 'echarts'
import { onMounted, ref, type Ref } from 'vue'

const chartDom: Ref<HTMLDivElement | null> = ref(null)

onMounted(() => {
  const chart = echarts.init(chartDom.value);
  chart.setOption({
    // 图表配置项
  });
})
</script>

然而在我们通常开发的中小型系统中,对于可视化图表的复杂性需求通常较低,一般只需要使用ECharts中的柱状图、折线图和饼图等基本图表类型,因此全量引入可能会显得有些资源浪费。在适当的情况下,可以考虑使用按需引入的方式,仅导入我们用到的图表组件:

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

<script setup>
import * as echarts from 'echarts/core'
import { BarChart } from 'echarts/charts'
import {
  // 标题组件
  TitleComponent,
  // 图例组件
  LegendComponent,
  // 提示框组件
  TooltipComponent,
  // 坐标系网格组件
  GridComponent
} from 'echarts/components'
import { onMounted, ref, type Ref } from 'vue'

echarts.use([ TitleComponent,
  LegendComponent,
  TooltipComponent,
  GridComponent,
  BarChart
])

const chartDom: Ref<HTMLDivElement | null> = ref(null)

onMounted(() => {
  const chart = echarts.init(chartDom.value);
  chart.setOption({
    // 图表配置项
  });
})
</script>

但是这样的按需引入在使用时也并不方便,每次使用都要引入很多组件,复用性比较差。因此我们可以将ECharts的按需引入封装成一个精简版的ECharts放在项目的utils文件夹下:

/* @/utils/echarts.ts */

import * as Echarts from 'echarts/core'
import { BarChart, PieChart, LineChart } from 'echarts/charts'
import {
  // 标题组件
  TitleComponent,
  // 图例组件
  LegendComponent,
  // 提示框组件
  TooltipComponent,
  // 坐标系网格组件
  GridComponent,
  // 数据集组件
  DatasetComponent,
  // 内置数据转换器组件 (filter, sort)
  TransformComponent,
  // 工具栏组件
  ToolboxComponent,
  // 区域缩放组件
  DataZoomComponent,
  // 原生图形元素组件
} from 'echarts/components'
import { LabelLayout, UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
import type {
  // 系列类型的定义后缀都为 SeriesOption
  BarSeriesOption,
  PieSeriesOption,
  LineSeriesOption
} from 'echarts/charts'
import type {
  // 组件类型的定义后缀都为 ComponentOption
  TitleComponentOption,
  TooltipComponentOption,
  GridComponentOption,
  DatasetComponentOption,
  ToolboxComponentOption,
  DataZoomComponentOption,
  GraphicComponentOption
} from 'echarts/components'
import type { ComposeOption } from 'echarts/core'

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

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

export const echarts = Echarts

这样在vue文件中需要使用ECharts时可以直接引入封装好的echarts.ts ,当增加了新的图表类型(如雷达图、热力图、桑基图等)时直接修改echarts.ts 文件就可以,提高使用按需引入的便捷性:

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

<script setup>
import { echarts, type ECOption } from '@/utils/echarts'
import { onMounted, ref, type Ref } from 'vue'

const chartDom: Ref<HTMLDivElement | null> = ref(null)

onMounted(() => {
  const chart = echarts.init(chartDom.value);
  const options: ECOption = {
    // 图表配置项
  };
  chart.setOption(options);
})
</script>


3.2 支持数据响应式更新

搞定了ECharts的资源引入后,我们就可以正式开始封装ECharts组件了。

我们以封装一个柱状图BarChart组件为例,首先,我们要让这个组件做到最基本的功能——以数据为驱动,支持响应式更新,即这个组件需要做到能够接收父组件传递的数据绘制成柱状图,当父组件数据变化时也要能重新渲染刷新图表。因此我们可以使用defineProps 定义一个data属性(y轴数据)和xAxisData属性(x轴数据),并结合监听器watch对这些属性进行监听,当监听到变化时用新的数据重新绘制柱状图:

/* @/components/BarChart.vue */

<template>
  <div ref="chartDom" style="height: 300px;"></div>
</template>

<script setup>
import { echarts, type ECOption } from '@/utils/echarts'
import { ref, shallowRef, watch, onMounted, type ShallowRef, type Ref } from 'vue'
import type { EChartsType } from 'echarts/types/dist/core'

//定义组件属性
const props = withDefaults(
  defineProps<{
    //数据
    data: Array<string | number>
    //x轴数据
    xAxisData: Array<string>
  }>(),
  {
    data: () => [],
    xAxisData: () => []
  }
)
//要渲染的Dom元素
const chartDom: Ref<HTMLDivElement | null> = ref(null)
//渲染的chart对象要用shallowRef
const chart: ShallowRef<EChartsType | null | undefined> = shallowRef(null)
//监听数据变化,重新绘制
watch(
  () => props,
  () => {
    drawChart()
  },
  {
    deep: true
  }
)

//绘制
function drawChart() {
  // 图表配置项
  const options: ECOption = {
    title: {
      text: 'ECharts柱状图'
    },
    tooltip: {},
    xAxis: {
      data: props.xAxisData
    },
    yAxis: {},
    series: [{
      name: '数量',
      type: 'bar',
      data: props.data
    }]
  };
  //开启notMerge保证数据不会叠加
  chart.value?.setOption(options, { notMerge: true });
}

onMounted(() => {
  chart.value = echarts.init(chartDom.value);
  drawChart()
})
</script>


📌注意:echarts.init 初始化得到的chart对象要定义成响应式数据时,得使用shallowRef来代替ref,不然会出现像tooltips不显示这样的问题

此时我们就得到了一个最简单的柱状图组件BarChart v1,可以完成最基本的数据展示:

<template>
  <el-card>
    <BarChart :data="data" :xAxisData="xData" />
  </el-card>
</template>

<script setup>
import BarChart from '@/components/charts/BarChart.vue';
import { ref } from 'vue'

const data = ref<Array<number>>([])
const xData = ref<Array<string>>([])

data.value = [5, 20, 36, 10, 10, 20]
xData.value = ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
</script>


在 Vue3 项目中封装 ECharts 组件技巧和思路

3.3 简化ECharts的配置工作

目前我们的BarChart v1组件只能支持单类柱状图的显示,当需要显示多类柱状图时它就无能为力了,因此我们需要扩展这个组件的属性,使它能够接收ECharts的配置数据:

import type { XAXisOption, YAXisOption, LegendComponentOption, BarSeriesOption, DataZoomComponentOption } from 'echarts/types/dist/shared';

const props = withDefaults(
  defineProps<{
    //数据
    data: Array<string | number>
    //x轴数据
    xAxisData: Array<string>
    //图表标题
    title?: string
    //系列配置
    series?: Array<BarSeriesOption>
    //x轴配置
    xAxis?: Array<XAXisOption>
    //y轴配置
    yAxis?: Array<YAXisOption>
    //图例配置
    legend?: LegendComponentOption
    //区域缩放配置
    dataZoom?: Array<DataZoomComponentOption>
  }>(),
  {
    data: () => [],
    xAxisData: () => []
  }
)


要使封装的ECharts组件更加易用,必须解决使用ECharts时存在的一个痛点——配置项繁多、配置工作繁琐。因此,我们进一步对BarChart组件进行改进,内置一些默认的ECharts配置,以简化组件的配置流程,从而提高开发效率:

/* @/components/BarChart.vue */

<template>
  <div ref="chartDom" :style="{ height: getHeight }"></div>
</template>

<script setup>
import { echarts, type ECOption } from '@/utils/echarts'
import { ref, shallowRef, watch, computed, onMounted, type ShallowRef, type Ref } from 'vue'
import type { EChartsType } from 'echarts/types/dist/core'
import type {
  XAXisOption, YAXisOption, LegendComponentOption, BarSeriesOption, DataZoomComponentOption
} from 'echarts/types/dist/shared'

//定义组件属性
const props = withDefaults(
  defineProps<{
    //数据
    data?: Array<string | number>
    //x轴数据
    xAxisData: Array<string>
    //图表标题
    title?: string
    //系列配置
    series?: Array<BarSeriesOption>
    //x轴配置
    xAxis?: Array<XAXisOption>
    //y轴配置
    yAxis?: Array<YAXisOption>
    //图例配置
    legend?: LegendComponentOption
    //区域缩放配置
    dataZoom?: Array<DataZoomComponentOption>
    //图形高度
    height?: number | string
  }>(),
  {
    data: () => [],
    xAxisData: () => [],
    title: 'ECharts柱状图',
  }
)
//要渲染的Dom元素
const chartDom: Ref<HTMLDivElement | null> = ref(null)
//渲染的chart对象要用shallowRef
const chart: ShallowRef<EChartsType | null | undefined> = shallowRef(null)
//高度同时支持string和number
const getHeight = computed(() => {
  return typeof props.height === 'number' ? props.height + 'px' : props.height
})
//监听数据变化,重新绘制
watch(
  () => props,
  () => {
    drawChart()
  },
  {
    deep: true
  }
)

//绘制
function drawChart() {
  let series: Array<BarSeriesOption> = props.series ? props.series : [{
    name: '数量',
    type: 'bar',
    barMaxWidth: 30,
    emphasis: { focus: 'self' },
    label: { show: true, position: 'inside', color: '#fff' },
    data: props.data
  }]

  let xAxis: Array<XAXisOption> = props.xAxis ? props.xAxis : [{
    type: 'category',
    axisTick: { show: false },
    data: props.xAxisData
  }]

  let yAxis: Array<YAXisOption> = props.yAxis ? props.yAxis : [{ type: 'value', minInterval: 1 }]

  let legend: LegendComponentOption = props.legend ? props.legend : {
    show: true,
    type: 'scroll',
    orient: 'horizontal',
    top: 25,
    left: 'center'
  }

  let dataZoom: Array<DataZoomComponentOption> = props.dataZoom ? props.dataZoom : []

  const options: ECOption = {
    backgroundColor: '',
    title: {
      text: props.title
    },
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'shadow'
      },
      // appendToBody:true
    },
    legend: legend,
    grid: {
      left: 10,
      right: 10,
      bottom: props.dataZoom ? 40 : 10,
      containLabel: true
    },
    toolbox: {
      show: true,
      feature: {
        magicType: { type: ['line', 'bar'] },
        dataView: { readOnly: false },
        saveAsImage: {}
      }
    },
    xAxis: xAxis,
    yAxis: yAxis,
    dataZoom: dataZoom,
    series: series
  }
  //开启notMerge保证配置数据不会叠加
  chart.value?.setOption(options, { notMerge: true });
}

onMounted(() => {
  chart.value = echarts.init(chartDom.value);
  drawChart()
})
</script>


现在我们得到了更加灵活的柱状图组件BarChart v2,支持传入ECharts配置数据,并且内置了默认配置,可以方便地实现多类型的柱状图显示:

<template>
  <el-card>
    <BarChart title="多类型柱状图" :height="500" :xAxisData="xData" :series="barSereis" />
  </el-card>
</template>

<script setup>
import BarChart from '@/components/charts/BarChart.vue';
import { ref } from 'vue'
import type { BarSeriesOption } from 'echarts/types/dist/shared'

const xData = ref<Array<string>>([])
const barSereis = ref<Array<BarSeriesOption>>([])

xData.value = ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
barSereis.value = [
  {
    name: '库存量',
    type: 'bar',
    barMaxWidth: 30,
    emphasis: { focus: 'series' },
    label: { show: true, position: 'top', color: 'inherit' },
    data: [5, 20, 36, 10, 10, 20]
  },
  {
    name: '销售量',
    type: 'bar',
    barMaxWidth: 30,
    emphasis: { focus: 'series' },
    label: { show: true, position: 'top', color: 'inherit' },
    data: [17, 43, 5, 28, 10, 36]
  }
]
</script>


在 Vue3 项目中封装 ECharts 组件技巧和思路

3.4 支持自适应窗口大小

此时的BarChart v2看起来似乎已经能满足使用了,但是当我们调整了浏览器窗口大小就会发现,我们的组件渲染出来的柱状图仍保持着初始的大小,会因为窗口大小的改变而出现留白或显示不全的问题。因此,我们还需要给BarChart组件加上resize 事件的监听,当监听到窗口大小变化时重新渲染ECharts图表。

查看ECharts提供的API会发现,它提供了一个resize 方法来重新渲染图表,我们可以结合window.addEventListener ,在项目的utils文件夹下再封装一个resize.ts工具来实现ECharts自适应窗口大小的功能:

/* @/utils/resize.ts */

//echarts图表自适应窗口变化封装方法
import { ref } from 'vue'
import { debounce } from 'lodash'

export default function () {
  //echarts图的实例
  const chartObject = ref()

  //使用防抖debounce函数,减少resize的次数
  const chartResizeHandler = debounce(() => {
    if (chartObject.value) {
      chartObject.value.resize()
    }
  }, 100)
  const initResizeEvent = () => {
    //添加窗口大小变化监听
    window.addEventListener('resize', chartResizeHandler)
  }
  const destroyResizeEvent = () => {
    //移除窗口大小变化监听
    window.removeEventListener('resize', chartResizeHandler)
  }

  const addResize = () => {
    initResizeEvent()
  }
  const removeResize = () => {
    destroyResizeEvent()
  }

  return {
    chartObject,
    addResize,
    removeResize
  }
}


有了这个工具类,再让BarChart组件实现窗口自适应就很方便了:

......
import resize from '@/utils/resize'

const { chartObject, addResize, removeResize } = resize()

onMounted(() => {
  chart.value = echarts.init(chartDom.value);
  drawChart()

  //添加窗口自适应
  chartObject.value = chart.value
  addResize()
})

onBeforeUnmount(() => {
  removeResize()
  chart.value?.dispose()
})


四、进一步提升组件的实用性和便捷性

通过上述步骤的封装,我们得到了一个基本的ECharts柱状图组件BarChart v3,已经可以满足常规的显示需求了,但是这个组件在数据接入后端接口时会存在一个问题:因为业务和开发人员的不同,后端接口返回的数据的属性名是不固定的,我们每次都要先把后端数据处理成纯数据数组传给组件才能显示。

在日常开发中,通常后端接口返回的数据形式是这样的:

[
  { name: '衬衫', saleNum: 17, stockNum: 5 },
  { name: '羊毛衫', saleNum: 43, stockNum: 20 },
  { name: '雪纺衫', saleNum: 5, stockNum: 36 },
  { name: '裤子', saleNum: 28, stockNum: 10 },
  { name: '高跟鞋', saleNum: 10, stockNum: 10 },
  { name: '袜子', saleNum: 36, stockNum: 20 }
]


对于这样的数据,我们的BarChart v3在使用时必须遍历重组成两个数组才能实现正常使用:

const xData = ref<Array<string>>([])
const barSereis = ref<Array<BarSeriesOption>>([])

//获取后端数据
function getData() {
  //假设此处请求后获取到如下数据
  let resData = [
    { name: '衬衫', saleNum: 17, stockNum: 5 },
    { name: '羊毛衫', saleNum: 43, stockNum: 20 },
    { name: '雪纺衫', saleNum: 5, stockNum: 36 },
    { name: '裤子', saleNum: 28, stockNum: 10 },
    { name: '高跟鞋', saleNum: 10, stockNum: 10 },
    { name: '袜子', saleNum: 36, stockNum: 20 }
  ]
  initChartData(resData)
}

//构建图表数据
function initChartData(data: any[]) {
  xData.value = []
  let saleData: number[] = [],
    stockData: number[] = []
  if (data) {
    data.forEach(item => {
      xData.value.push(item.name)
      saleData.push(item.saleNum)
      stockData.push(item.stockNum)
    })
  }
  barSereis.value = [
    {
      name: '库存量',
      type: 'bar',
      barMaxWidth: 30,
      emphasis: { focus: 'series' },
      label: { show: true, position: 'top', color: 'inherit' },
      data: saleData
    },
    {
      name: '销售量',
      type: 'bar',
      barMaxWidth: 30,
      emphasis: { focus: 'series' },
      label: { show: true, position: 'top', color: 'inherit' },
      data: stockData
    }
  ]
}

getData()


而对于不同的图形和不同的接口,我们都得进行不同的遍历,这样一来就增加了使用组件的额外重复工作,说明目前这个组件还是不太通用,这时候就需要考虑使用ECharts的数据集(dataset)配置了。

4.1 巧用dataset

在ECharts中,数据集(dataset)是专门用来管理数据的组件。虽然每个系列都可以在 series.data 中设置数据,但是从 ECharts4 支持数据集开始,更推荐使用数据集来管理数据。因为这样数据可以被多个组件复用,也方便进行 “数据和其他配置” 分离的配置风格。毕竟在运行时,数据是最常改变的,而其他配置大多并不会改变。通过巧妙地使用ECharts的dataset组件,可以更方便地处理数据,实现更灵活的图表展示。

对于上述示例的接口数据,使用dataset来构建有两种比较简便的方式。

一种方式是配置dimensions让数据自动按顺序映射到坐标轴中:

option = {
  legend: {},
  tooltip: {},
  dataset: {
    /* 
      用 dimensions 指定了维度的顺序。直角坐标系中,如果 X 轴 type 为 category,
      默认把第一个维度映射到 X 轴上,后面维度映射到 Y 轴上。
    */
    dimensions: ['name', 'stockNum', 'saleNum'],
    source: [
      { name: '衬衫', saleNum: 17, stockNum: 5 },
      { name: '羊毛衫', saleNum: 43, stockNum: 20 },
      { name: '雪纺衫', saleNum: 5, stockNum: 36 },
      { name: '裤子', saleNum: 28, stockNum: 10 },
      { name: '高跟鞋', saleNum: 10, stockNum: 10 },
      { name: '袜子', saleNum: 36, stockNum: 20 }
    ]
  },
  xAxis: { type: 'category' },
  yAxis: {},
  series: [
    {
      name: '库存量',
      type: 'bar',
      barMaxWidth: 30,
      emphasis: { focus: 'series' },
      label: { show: true, position: 'top', color: 'inherit' }
    },
    {
      name: '销售量',
      type: 'bar',
      barMaxWidth: 30,
      emphasis: { focus: 'series' },
      label: { show: true, position: 'top', color: 'inherit' }
    }
  ]
};


另一种方式是配置series.encode,让每个图例按配置映射:

option = {
  legend: {},
  tooltip: {},
  dataset: {
    source: [
      { name: '衬衫', saleNum: 17, stockNum: 5 },
      { name: '羊毛衫', saleNum: 43, stockNum: 20 },
      { name: '雪纺衫', saleNum: 5, stockNum: 36 },
      { name: '裤子', saleNum: 28, stockNum: 10 },
      { name: '高跟鞋', saleNum: 10, stockNum: 10 },
      { name: '袜子', saleNum: 36, stockNum: 20 }
    ]
  },
  xAxis: { type: 'category' },
  yAxis: {},
  series: [
    {
      name: '库存量',
      type: 'bar',
      barMaxWidth: 30,
      emphasis: { focus: 'series' },
      label: { show: true, position: 'top', color: 'inherit' },
      encode: { x:'name', y: 'stockNum' }
    },
    {
      name: '销售量',
      type: 'bar',
      barMaxWidth: 30,
      emphasis: { focus: 'series' },
      label: { show: true, position: 'top', color: 'inherit' },
      encode: { x:'name', y: 'saleNum' }
    }
  ]
};


📌关于dataset的更多使用方式,可以参考ECharts文档:echarts.apache.org/handbook/zh…

考虑到数据的兼容性和使用的复杂性,在这里我们可以使用第二种配置series.encode的方式来改造BarChart组件,使其支持使用dataset:

//定义组件属性
const props = withDefaults(
  defineProps<{
    //数据
    data?: Array<string | number>
    //x轴数据
    xAxisData: Array<string>
    //图表标题
    title?: string
    //系列配置
    series?: Array<BarSeriesOption>
    //x轴配置
    xAxis?: Array<XAXisOption>
    //y轴配置
    yAxis?: Array<YAXisOption>
    //图例配置
    legend?: LegendComponentOption
    //区域缩放配置
    dataZoom?: Array<DataZoomComponentOption>
    //图形高度
    height?: number | string
    //数据集
    datasetSource?: Array<any>
  }>(),
  {
    data: () => [],
    xAxisData: () => [],
    title: 'ECharts柱状图',
  }
)
......

//绘制
function drawChart() {
  ......
  const options: ECOption = {
    ......
    dataset: {
      source: props.datasetSource
    },
    series: series
  }
  //开启notMerge保证配置数据不会叠加
  chart.value?.setOption(options, { notMerge: true });
}


此时我们再接入后端接口时,就不需要再重组数据了:

<template>
  <el-card>
    <BarChart title="多类型柱状图" :height="500" :xAxisData="xData" :datasetSource="dataset" :series="barSereis" />
  </el-card>
</template>

<script setup>
import BarChart from '@/components/charts/BarChart.vue';
import { ref } from 'vue'
import type { BarSeriesOption } from 'echarts/types/dist/shared'

const xData = ref<Array<string>>([])
const dataset = ref<Array<any>>([])
const barSereis = ref<Array<BarSeriesOption>>([])

//获取后端数据
function getData() {
  //假设此处请求后获取到如下数据
  let resData = [
    { name: '衬衫', saleNum: 17, stockNum: 5 },
    { name: '羊毛衫', saleNum: 43, stockNum: 20 },
    { name: '雪纺衫', saleNum: 5, stockNum: 36 },
    { name: '裤子', saleNum: 28, stockNum: 10 },
    { name: '高跟鞋', saleNum: 10, stockNum: 10 },
    { name: '袜子', saleNum: 36, stockNum: 20 }
  ]
  initChartData(resData)
}

//构建图表数据
function initChartData(data: any[]) {
  if (data) {
    dataset.value = data
    xData.value = []
    data.forEach(item => {
      xData.value.push(item.name)
    })
  }
}

barSereis.value = [
  {
    name: '库存量',
    type: 'bar',
    barMaxWidth: 30,
    emphasis: { focus: 'series' },
    label: { show: true, position: 'top', color: 'inherit' },
    encode: { x: 'name', y: 'stockNum' }
  },
  {
    name: '销售量',
    type: 'bar',
    barMaxWidth: 30,
    emphasis: { focus: 'series' },
    label: { show: true, position: 'top', color: 'inherit' },
    encode: { x: 'name', y: 'saleNum' }
  }
]

getData()
</script>


在 Vue3 项目中封装 ECharts 组件技巧和思路

4.2 结合axios请求进一步封装

在BarChart v4中,虽然我们支持了适配不同属性名的后端数据,但是组件的配置内容还可以再精简,比如像用于构建图表数据的initChartData函数和柱状图系列配置barSereis,我们在使用时完全不关心它的生成过程,似乎可以完全集成到BarChart组件内部。

同时在一般项目的开发过程中,我们获取图表的后端数据时,往往都是一个图表对应一个接口,那么在后端接口规范统一且数据可直接使用的情况下,我们也许可以让BarChart绑定一个aixios方法,直接从该方法中获取数据集。

根据这个思路,我们可以进一步再改造一下BarChart组件,增加一个options属性:

import type { ChartSetting } from '@/types/ChartData'

//定义组件属性
const props = withDefaults(
  defineProps<{
    //数据
    data?: Array<string | number>
    //x轴数据
    xAxisData: Array<string>
    //图表标题
    title?: string
    //系列配置
    series?: Array<BarSeriesOption>
    //x轴配置
    xAxis?: Array<XAXisOption>
    //y轴配置
    yAxis?: Array<YAXisOption>
    //图例配置
    legend?: LegendComponentOption
    //区域缩放配置
    dataZoom?: Array<DataZoomComponentOption>
    //图形高度
    height?: number | string
    //数据集
    datasetSource?: Array<any>
    //综合配置
    options?: ChartSetting
  }>(),
  {
    data: () => [],
    xAxisData: () => [],
    title: 'ECharts柱状图',
  }
)
......

//绘制
async function drawChart() {
  let datasetSource: Array<any> | undefined = props.datasetSource,
    series: Array<BarSeriesOption> = [],
    xAxisData: Array<string> = props.xAxisData
  if (props.options) {
    if (props.options.apiMethod) {
      //获取接口数据作为数据集
      datasetSource = await props.options.apiMethod()
      if (props.options.xProp) {
        //根据配置的x轴属性名生成x轴数据
        xAxisData = []
        datasetSource?.forEach(data => {
          xAxisData.push(data[props.options.xProp])
        })
      }
    }
    if (props.options.sereisOption) {
      props.options.sereisOption.forEach(opt => {
        series.push({
          name: '数量',
          type: 'bar',
          barMaxWidth: 30,
          emphasis: { focus: 'series' },
          label: { show: true, position: 'top', color: 'inherit' },
          ...opt
        })
      })
    }
  } else {
    series = props.series ? props.series : [{
      name: '数量',
      type: 'bar',
      barMaxWidth: 30,
      emphasis: { focus: 'self' },
      label: { show: true, position: 'inside', color: '#fff' },
      data: props.data
    }]
  }

  let xAxis: Array<XAXisOption> = props.xAxis ? props.xAxis : [{
    type: 'category',
    axisTick: { show: false },
    data: xAxisData
  }]

  ......
  const options: ECOption = {
    ......
    dataset: {
      source: datasetSource
    },
    series: series
  }
  //开启notMerge保证配置数据不会叠加
  chart.value?.setOption(options, { notMerge: true });
}


其中ChartSetting是一个自定义类型,放在了项目types目录下的ChartData.ts中:

/* @/types/ChartData.ts */

export interface SeriesData {
  name?: string
  data?: number[]
  color?: string
  yAxisIndex?: number,
  radius?: string | string[],
  itemStyle?: any,
  encode?: {
    x?: string
    y?: string
    itemName?: string
    value?: string
  }
}

export interface ChartSetting {
  //api接口方法
  apiMethod: Function
  // x轴属性名
  xProp: string
  //图例配置
  sereisOption: SeriesData[]
}


现在再调用柱状图组件就更进一步简化了配置过程:

<template>
  <el-card>
    <template #header>
      <el-button type="primary" @click="search">查询</el-button>
    </template>
    <BarChart title="多类型柱状图" :height="500" :options="chartOption" />
  </el-card>
</template>

<script setup>
import BarChart from '@/components/charts/BarChart.vue'
import type { ChartSetting } from '@/types/ChartData'
import { ref } from 'vue'

//查询参数
const queryData = ref<any>({
  count: 0
})
const chartOption = ref<ChartSetting>()

//获取数据接口
function getDataApi(params?: any) {
  //模拟axios请求返回的数据
  //return axios.request(...)
  return new Promise((resolve) => {
    params.count++
    let randomInt = Math.floor(Math.random() * 50 + params.count)
    resolve([
      { name: '衬衫', saleNum: 17 + randomInt, stockNum: 5 + randomInt },
      { name: '羊毛衫', saleNum: 43 + randomInt, stockNum: 20 + randomInt },
      { name: '雪纺衫', saleNum: 5 + randomInt, stockNum: 36 + randomInt },
      { name: '裤子', saleNum: 28 + randomInt, stockNum: 10 + randomInt },
      { name: '高跟鞋', saleNum: 10 + randomInt, stockNum: 10 + randomInt },
      { name: '袜子', saleNum: 36 + randomInt, stockNum: 20 + randomInt }
    ])
  })
}

function search() {
  chartOption.value = {
    apiMethod: () => getDataApi(queryData.value),
    xProp: 'name',
    sereisOption: [
      { name: '库存量', encode: { x: 'name', y: 'stockNum' } },
      { name: '销售量', encode: { x: 'name', y: 'saleNum' } },
    ]
  }
}

search()
</script>


在 Vue3 项目中封装 ECharts 组件技巧和思路

将api封装进柱状图组件的好处是,当我们要实现类似这样的页面时:

在 Vue3 项目中封装 ECharts 组件技巧和思路

只需要进行简单的配置就可以完成:

<template>
  <el-row :gutter="16">
    <el-col v-for="(item, index) in chartOptionList" :key="index" :lg="12" style="margin-bottom: 10px;">
      <el-card>
        <BarChart :title="item.title" :height="item.height || 300" :options="item.chartOption" />
      </el-card>
    </el-col>
  </el-row>
</template>

<script setup>
import BarChart from '@/components/charts/BarChart.vue'
import type { ChartSetting } from '@/types/ChartData'
import { ref } from 'vue'
//axios api请求方法
import { getDataApi1, getDataApi2, getDataApi3, getDataApi4 } from '@/assets/api/chartAPI'

interface ChartCard {
  //标题
  title?: string,
  //高度
  height?: number,
  //图表y轴配置
  yAxis?: Array<any>
  chartOption: ChartSetting
}
const chartOptionList = ref<ChartCard[]>([
  {
    title: '图1',
    chartOption: {
      apiMethod: () => getDataApi1(),
      xProp: 'name',
      sereisOption: [
        { name: '数量', encode: { x: 'name', y: 'count' } }
      ]
    }
  },
  {
    title: '图2',
    chartOption: {
      apiMethod: () => getDataApi2(),
      xProp: 'date',
      sereisOption: [
        { name: '数量', encode: { x: 'date', y: 'count' } },
      ]
    }
  },
  {
    title: '图3',
    chartOption: {
      apiMethod: () => getDataApi3(),
      xProp: 'name',
      sereisOption: [
        { name: '库存量', encode: { x: 'name', y: 'stockNum' } },
        { name: '销售量', encode: { x: 'name', y: 'saleNum' } },
      ]
    }
  },
  {
    title: '图4',
    chartOption: {
      apiMethod: () => getDataApi4(),
      xProp: 'date',
      sereisOption: [
        { name: '剩余量', encode: { x: 'date', y: 'num1' } },
        { name: '进货量', encode: { x: 'date', y: 'num2' } },
      ]
    }
  },
])
</script>


五、总结与后期改进

在进行了一系列的完善后,最终我们得到了这样一个柱状图组件:

/* @/components/BarChart.vue */

<template>
  <div ref="chartDom" :style="{ height: getHeight }"></div>
</template>

<script setup>
import { echarts, type ECOption } from '@/utils/echarts'
import { ref, shallowRef, watch, computed, onMounted, onBeforeUnmount, type ShallowRef, type Ref } from 'vue'
import type { EChartsType } from 'echarts/types/dist/core'
import type {
  XAXisOption, YAXisOption, LegendComponentOption, BarSeriesOption, DataZoomComponentOption
} from 'echarts/types/dist/shared'
import resize from '@/utils/resize'
import type { ChartSetting } from '@/types/ChartData'

//定义组件属性
const props = withDefaults(
  defineProps<{
    //数据
    data?: Array<string | number>
    //x轴数据
    xAxisData?: Array<string>
    //图表标题
    title?: string
    //系列配置
    series?: Array<BarSeriesOption>
    //x轴配置
    xAxis?: Array<XAXisOption>
    //y轴配置
    yAxis?: Array<YAXisOption>
    //图例配置
    legend?: LegendComponentOption
    //区域缩放配置
    dataZoom?: Array<DataZoomComponentOption>
    //图形高度
    height?: number | string
    //数据集
    datasetSource?: Array<any>
    //综合配置
    options?: ChartSetting
  }>(),
  {
    data: () => [],
    xAxisData: () => [],
    title: 'ECharts柱状图',
  }
)
//要渲染的Dom元素
const chartDom: Ref<HTMLDivElement | null> = ref(null)
//渲染的chart对象要用shallowRef
const chart: ShallowRef<EChartsType | null | undefined> = shallowRef(null)
//高度同时支持string和number
const getHeight = computed(() => {
  return typeof props.height === 'number' ? props.height + 'px' : props.height
})
//监听数据变化,重新绘制
watch(
  () => props,
  () => {
    drawChart()
  },
  { deep: true }
)

//绘制
async function drawChart() {
  let datasetSource: Array<any> | undefined = props.datasetSource,
    series: Array<BarSeriesOption> = [],
    xAxisData: Array<string> = props.xAxisData
  if (props.options) {
    if (props.options.apiMethod) {
      //获取接口数据作为数据集
      datasetSource = await props.options.apiMethod()
      if (props.options.xProp) {
        //根据配置的x轴属性名生成x轴数据
        xAxisData = []
        datasetSource?.forEach(data => {
          xAxisData.push(data[props.options.xProp])
        })
      }
    }
    if (props.options.sereisOption) {
      props.options.sereisOption.forEach(opt => {
        series.push({
          name: '数量',
          type: 'bar',
          barMaxWidth: 30,
          emphasis: { focus: 'series' },
          label: { show: true, position: 'top', color: 'inherit' },
          ...opt
        })
      })
    }
  } else {
    series = props.series ? props.series : [{
      name: '数量',
      type: 'bar',
      barMaxWidth: 30,
      emphasis: { focus: 'self' },
      label: { show: true, position: 'inside', color: '#fff' },
      data: props.data
    }]
  }

  let xAxis: Array<XAXisOption> = props.xAxis ? props.xAxis : [{
    type: 'category',
    axisTick: { show: false },
    data: xAxisData
  }]

  let yAxis: Array<YAXisOption> = props.yAxis ? props.yAxis : [{ type: 'value', minInterval: 1 }]

  let legend: LegendComponentOption = props.legend ? props.legend : {
    show: true,
    type: 'scroll',
    orient: 'horizontal',
    top: 25,
    left: 'center'
  }

  let dataZoom: Array<DataZoomComponentOption> = props.dataZoom ? props.dataZoom : []

  const options: ECOption = {
    backgroundColor: '',
    title: {
      text: props.title
    },
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'shadow'
      },
      // appendToBody:true
    },
    legend: legend,
    grid: {
      left: 10,
      right: 10,
      bottom: props.dataZoom ? 40 : 10,
      containLabel: true
    },
    toolbox: {
      show: true,
      feature: {
        magicType: { type: ['line', 'bar'] },
        dataView: { readOnly: false },
        saveAsImage: {}
      }
    },
    xAxis: xAxis,
    yAxis: yAxis,
    dataZoom: dataZoom,
    dataset: {
      source: datasetSource
    },
    series: series
  }
  //开启notMerge保证配置数据不会叠加
  chart.value?.setOption(options, { notMerge: true });
}

const { chartObject, addResize, removeResize } = resize()
onMounted(() => {
  chart.value = echarts.init(chartDom.value);
  drawChart()

  //添加窗口自适应
  chartObject.value = chart.value
  addResize()
})

onBeforeUnmount(() => {
  removeResize()
  chart.value?.dispose()
})
</script>


这个组件支持仅传入横、纵坐标数据来显示基础的柱状图,也支持传入JSON配置来显示多系列的复杂柱状图,足以应对日常基本需求。同样地,利用类似的设计逻辑,我们也能轻松地封装出饼图、折线图和热力图等组件。

不过在实际项目应用中,特别是面对大屏展示等复杂多变的可视化需求时,现有的封装形式可能还略显不足,因此仍有待进一步拓展和优化。在后续的改进中,我们可以进一步优化组件的功能和性能,以满足不同项目的需求:比如可以为这个组件扩展更多的动态配置项属性,如tooltip、grid等,使其可以更灵活地使用;也可以对series属性做更好的适配,将所有的图表组件整合为一个,仅通过配置不同的sereis.type,就可以让这个组件展示柱状图、饼图或折线图等。

在Vue 3中封装ECharts组件无疑为前端开发人员提供了一种高效、便捷的方式来构建可视化图表,极大地提升了开发效率和代码复用性。但值得注意的是,封装组件更适合用于处理基础图表的构建,而在面对高度定制化的ECharts图表时,过度依赖封装可能会增加代码的复杂性和维护成本。因此,是否选择封装ECharts组件应根据具体项目需求进行权衡。

本文是在借鉴现有ECharts组件封装经验的基础上,提供了一种可行的封装的技巧和思路,也许并不是最优的解决方案。希望通过这篇文章能够启发大家的思考,帮助大家更便捷地在Vue 3项目中使用ECharts。

0条评论

您的电子邮件等信息不会被公开,以下所有项均必填

OK! You can skip this field.