关于在Vue3项目中封装ECharts组件的一种思路

目录

一、相关知识

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中的所有图表和组件,使用起来比较便捷:



<script setup lang="ts">
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中的柱状图、折线图和饼图等基本图表类型,因此全量引入可能会显得有些资源浪费。在适当的情况下,可以考虑使用按需引入的方式,仅导入我们用到的图表组件:



<script setup lang="ts">
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文件夹下:



import * as Echarts from 'echarts/core'
import { BarChart, PieChart, LineChart } from 'echarts/charts'
import {
  
  TitleComponent,
  
  LegendComponent,
  
  TooltipComponent,
  
  GridComponent,
  
  DatasetComponent,
  
  TransformComponent,
  
  ToolboxComponent,
  
  DataZoomComponent,
  
} from 'echarts/components'
import { LabelLayout, UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
import type {
  
  BarSeriesOption,
  PieSeriesOption,
  LineSeriesOption
} from 'echarts/charts'
import type {
  
  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
])


export type ECOption = ComposeOption<
  | BarSeriesOption
  | PieSeriesOption
  | LineSeriesOption
  | TitleComponentOption
  | TooltipComponentOption
  | GridComponentOption
  | DatasetComponentOption
  | ToolboxComponentOption
  | DataZoomComponentOption
  | GraphicComponentOption
>

export const echarts = Echarts

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



<script setup lang="ts">
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对这些属性进行监听,当监听到变化时用新的数据重新绘制柱状图:






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

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




柱状图组件BarChart v1使用效果

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>
    
    xAxisData: Array<string>
    
    title?: string
    
    series?: Array<BarSeriesOption>
    
    xAxis?: Array<XAXisOption>
    
    yAxis?: Array<YAXisOption>
    
    legend?: LegendComponentOption
    
    dataZoom?: Array<DataZoomComponentOption>
  }>(),
  {
    data: () => [],
    xAxisData: () => []
  }
)

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






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




柱状图组件BarChart v2使用效果

3.4 支持自适应窗口大小

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

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




import { ref } from 'vue'
import { debounce } from 'lodash'

export default function () {
  
  const chartObject = ref()

  
  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: ['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>
    
    xAxisData: Array<string>
    
    title?: string
    
    series?: Array<BarSeriesOption>
    
    xAxis?: Array<XAXisOption>
    
    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
  }
  
  chart.value?.setOption(options, { notMerge: true });
}

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




柱状图组件BarChart v4使用效果

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>
    
    xAxisData: Array<string>
    
    title?: string
    
    series?: Array<BarSeriesOption>
    
    xAxis?: Array<XAXisOption>
    
    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) {
        
        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
  }
  
  chart.value?.setOption(options, { notMerge: true });
}

其中ChartSetting是一个自定义类型,放在了项目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 {
  
  apiMethod: Function
  
  xProp: string
  
  sereisOption: SeriesData[]
}

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




柱状图组件BarChart v5使用效果

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

多echarts图页面示例

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




五、总结与后期改进

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






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

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

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

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

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=20824,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?