目录
一、相关知识
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 组件有以下几个好处:
简化使用:封装后的组件可以提供更简洁、更易用的 API,我们能够更快速地完成页面中 ECharts图表的绘制。
隐藏实现细节:封装可以隐藏 ECharts的具体实现细节,让我们不必过多关心图表配置项和底层实现,只需关注如何处理数据来进行展示。
提高复用性:封装后的组件可以被多个页面或项目共享使用,提高了代码的复用性和可维护性。
增强扩展性:封装可以在原有基础上进行功能扩展,比如添加自定义交互、动画效果等,从而满足更多定制化的需求。
提高可维护性:封装可以将相关的代码逻辑集中在一个组件中,便于维护和管理,减少代码冗余和维护成本。
通过封装 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>
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>
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>
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>
将api封装进柱状图组件的好处是,当我们要实现类似这样的页面时:
只需要进行简单的配置就可以完成:
<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。