目录
一、相关知识
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中的所有图表和组件,使用起来比较便捷:
<div ref="chartDom">div>
<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中的柱状图、折线图和饼图等基本图表类型,因此全量引入可能会显得有些资源浪费。在适当的情况下,可以考虑使用按需引入的方式,仅导入我们用到的图表组件:
<div ref="chartDom">div>
<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
文件就可以,提高使用按需引入的便捷性:
<div ref="chartDom">div>
<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
对这些属性进行监听,当监听到变化时用新的数据重新绘制柱状图:
<div ref="chartDom" style="height: 300px;">div>
📌注意:
echarts.init
初始化得到的chart对象要定义成响应式数据时,得使用shallowRef来代替ref,不然会出现像tooltips不显示这样的问题
此时我们就得到了一个最简单的柱状图组件BarChart v1,可以完成最基本的数据展示:
<el-card>
<BarChart :data="data" :xAxisData="xData" />
el-card>
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配置,以简化组件的配置流程,从而提高开发效率:
<div ref="chartDom" :style="{ height: getHeight }">div>
现在我们得到了更加灵活的柱状图组件BarChart v2,支持传入ECharts配置数据,并且内置了默认配置,可以方便地实现多类型的柱状图显示:
<el-card>
<BarChart title="多类型柱状图" :height="500" :xAxisData="xData" :series="barSereis" />
el-card>
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 });
}
此时我们再接入后端接口时,就不需要再重组数据了:
<el-card>
<BarChart title="多类型柱状图" :height="500" :xAxisData="xData" :datasetSource="dataset" :series="barSereis" />
el-card>
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[]
}
现在再调用柱状图组件就更进一步简化了配置过程:
<el-card>
<template #header>
<el-button type="primary" @click="search">查询el-button>
template>
<BarChart title="多类型柱状图" :height="500" :options="chartOption" />
el-card>
将api封装进柱状图组件的好处是,当我们要实现类似这样的页面时:
只需要进行简单的配置就可以完成:
<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>
五、总结与后期改进
在进行了一系列的完善后,最终我们得到了这样一个柱状图组件:
<div ref="chartDom" :style="{ height: getHeight }">div>
这个组件支持仅传入横、纵坐标数据来显示基础的柱状图,也支持传入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