关于在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 组件有以下几个好处:
- 简化使用:封装后的组件可以提供更简洁、更易用的 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 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中的柱状图、折线图和饼图等基本图表类型,因此全量引入可能会显得有些资源浪费。在适当的情况下,可以考虑使用按需引入的方式,仅导入我们用到的图表组件:
<template>
<div ref="chartDom"></div>
</template>
<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
文件夹下:
/* @/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 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
对这些属性进行监听,当监听到变化时用新的数据重新绘制柱状图:
/* @/components/BarChart.vue */
<template>
<div ref="chartDom" style="height: 300px;"></div>
</template>
<script setup lang="ts">
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 lang="ts">
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 lang="ts">
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 lang="ts">
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 lang="ts">
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 lang="ts">
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 lang="ts">
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 lang="ts">
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。