一、介绍
在现在开发的项目中, ECharts 已成为不可或缺的可视化工具,通常我们会将 ECharts 封装为组件以便重复使用,但随着项目规模扩大,图表的使用频率增加,代码重复和维护成本也随之上升,甚至影响开发效率。如何简化 ECharts 的使用,减少冗余代码,成为开发者们需要解决的问题。
本文将带你一步步封装一个简单的 ECharts Hook,帮助你轻松管理图表逻辑,实现代码复用,提升开发效率。
二、介绍使用 ECharts 常用方式
ECharts 初始化
在使用 ECharts 时,首先需要初始化一个图表实例,将其绑定到页面上的 DOM 元素上。初始化是将 ECharts 绑定到特定的 DOM 容器(通常是一个 div
标签),并使用 echarts.init()
方法生成图表实例。
echarts.init(HTMLElement);
ECharts 中 options 修改
ECharts 图表配置通过 setOption
方法进行更新。每当需要修改图表中的数据、样式或其他配置时,都可以通过此方法动态更新 options
。
// 当数据发生变化时,实时更新 ECharts
this.chart.setOption(EChartsOption);
ECharts resize
ECharts 图表在页面大小变化时需要自动调整尺寸。为此,ECharts 提供了 resize()
方法,可以监听浏览器窗口的变化并自适应调整图表大小。
window.addEventListener('resize', handleResize);
代码冗余介绍
在编写 ECharts 可视化时,以上操作几乎是必不可少的。然而,当项目中需要展示多个图表时,我们往往会选择为每种图表单独封装组件,如下所示:
BarChart.vue => 初始化 、修改 options 内容 、resize 、请求后端接口
LineChart.vue => 初始化 、修改 options 内容 、resize 、请求后端接口
PieChart.vue => 初始化 、修改 options 内容 、resize 、请求后端接口
RaddarChart.vue => 初始化 、修改 options 内容 、resize 、请求后端接口
这种方式虽然直观,但随着图表数量的增加,代码重复问题愈发严重。每个组件都需要进行类似的初始化、配置修改、尺寸调整以及数据请求,导致维护成本大幅上升。一旦需求发生变更,每个组件都可能需要逐一修改,既浪费时间,又容易引发错误。
三、封装 useECharts
1. 编码
看完代码后,我们来说说注意事项以及使用的方法。
// hooks/useECharts.ts
import * as echarts from 'echarts';
import {Ref} from "vue";
export interface ChartStrategy {
getOptions: () => echarts.EChartsOption;
} // 定义一个策略接口 , 方便具体策略实现使用
// 自定义的 Vue 钩子函数,用于管理 ECharts 实例
export function useECharts(
chartRef: Ref<HTMLElement | null>, // chartRef 是一个引用,指向包含图表的 DOM 元素
initOptions: echarts.EChartsOption, // strategy 是一个接口,提供获取图表配置的方法
theme?: string | object | null, // 可选的 theme 参数,用于设置 ECharts 主题
opts?: echarts.EChartsInitOpts // 可选的 opts 参数,用于初始化 ECharts 的配置
) {
// 使用 shallowRef 创建一个响应式引用,用于保存 ECharts 实例
const chartInstance = shallowRef<echarts.EChartsType | null>(null);
// 使用 ref 创建一个响应式引用,用于保存图表的配置选项
const options = reactive<echarts.EChartsOption>(initOptions as echarts.EChartsOption);
// 初始化图表实例的函数
const initChart = () => {
// 确保 chartRef 绑定了 DOM 元素且 chartInstance 尚未初始化
if (chartRef.value && !chartInstance.value) {
// 初始化 ECharts 实例并赋值给 chartInstance
chartInstance.value = echarts.init(chartRef.value, theme, opts);
// 设置图表的初始选项
chartInstance.value.setOption(options);
}
};
// 更新图表配置选项的函数
const updateChartOptions = (newOptions: echarts.EChartsOption) => {
if (chartInstance.value) {
// 使用新的选项更新图表,不合并现有选项并延迟更新以优化性能
chartInstance.value.setOption(newOptions, {notMerge: true, lazyUpdate: true});
}
};
// 处理窗口大小调整的函数,确保图表能够自动调整大小
const handleResize = () => {
chartInstance.value?.resize(); // 如果 chartInstance 存在,则调用 resize 方法
};
// 销毁图表实例的函数,释放内存并清空引用
const disposeChart = () => {
chartInstance.value?.dispose(); // 调用 ECharts 的 dispose 方法销毁实例
chartInstance.value = null; // 清空 chartInstance 引用,避免内存泄漏
};
// 监听 options 的变化,并在其发生改变时更新图表
watch(options as echarts.EChartsOption, updateChartOptions, {deep: true});
// 组件挂载时初始化图表并添加窗口大小调整的事件监听器
onMounted(() => {
initChart(); // 初始化图表
window.addEventListener('resize', handleResize); // 监听窗口大小变化
});
// 组件卸载时移除事件监听器并销毁图表实例
onUnmounted(() => {
window.removeEventListener('resize', handleResize); // 移除窗口大小调整的监听器
disposeChart(); // 销毁图表实例
});
// 组件激活时重新初始化图表并添加事件监听器
onActivated(() => {
if (!chartInstance.value) {
initChart(); // 如果图表实例不存在,重新初始化
}
window.addEventListener('resize', handleResize); // 监听窗口大小变化
});
// 组件停用时移除事件监听器并销毁图表实例
onDeactivated(() => {
window.removeEventListener('resize', handleResize); // 移除窗口大小调整的监听器
disposeChart(); // 销毁图表实例
});
// 返回 chartInstance 和相关的控制方法,供外部组件使用
return {
chartInstance, // 返回图表实例的引用
options, // 返回图表配置选项的引用
initChart, // 返回初始化图表的方法
handleResize, // 返回手动触发图表调整大小的方法
disposeChart, // 返回手动销毁图表实例的方法
};
}
2. 注意事项
这里大家可能会疑惑,为什么不直接使用 Ref , 而是使用 shallowRef ,那让我们直接换成Ref试试,当我们把 shallowRef 替换为 Ref ,如下 :
const chartInstance = ref<echarts.EChartsType | null>(null);
结果在运行的时候出现了问题 :
解释一下 :
在 Vue 中,ref
和 reactive
都会为被包装的对象提供深度的响应式监听,可以会导致ECharts中的部分属性无法直接获取。这意味着,如果你用 ref
或 reactive
包装一个 ECharts 实例,Vue 将尝试监视该实例的所有内部属性的变化。但 ECharts 实例是一个非常复杂的对象,包含了大量与 DOM 操作、渲染引擎相关的属性和方法。对这些进行深度监听会带来额外的性能开销,而这些变化大多数情况下并不需要被监视。
使用 shallowRef
只会对 ECharts 实例的顶层进行响应式监听,而不会深入到其内部。这能避免不必要的计算和性能消耗,从而提高应用的效率。而 ECharts 实例本身并不需要频繁响应式地监视,因为大部分操作(如初始化、数据更新、调整大小)都是通过显式的 setOption
或 resize
方法完成的。通过这些方法,你可以手动触发图表的变化,因此不需要 Vue 的响应式系统来监视实例的每一个细节。
3. 优势
- 我们可以结合之前封装组件的方法与当前hooks结合,更加方便。
- 当有业务增加的时候,比如我们需要一个图片下载功能,可以直接在Hooks中多暴露一个方法,如下 :
// 省略其他代码
export function useECharts(
chartRef: Ref<HTMLElement | null>, // chartRef 是一个引用,指向包含图表的 DOM 元素
initOptions: echarts.EChartsOption, // strategy 是一个接口,提供获取图表配置的方法
theme?: string | object | null, // 可选的 theme 参数,用于设置 ECharts 主题
opts?: echarts.EChartsInitOpts // 可选的 opts 参数,用于初始化 ECharts 的配置
) {
// 下载图表为图片
const downloadChartImage = (filename = 'chart.png') => {
if (chartInstance.value) {
const base64 = chartInstance.value.getDataURL({
type: 'png',
pixelRatio: 2,
backgroundColor: '#fff'
});
const link = document.createElement('a');
link.href = base64;
link.download = filename;
link.click();
}
};
return {
downloadChartImage
};
}
- 其他优点仁者见仁智者见智 ( * ^ ▽ ^ * )
四、让我们快速上手一下这个hooks
1. 基础使用
根据useECharts
传入即可,不介绍
2. 使用 hooks 中的 ChartStrategy 管理
先在公共类中定义一些策略
// 省略其他策略
class LineOptionStrategy implements ChartStrategy {
getOptions() {
return {
xAxis: {
type: 'category',
data: ['', '', '', '', '', '', '']
},
yAxis: {
type: 'value'
},
series: [
{
data: ["1", "1", "2", "1", "1", "1", "1"],
type: 'line'
}
]
}
}
}
在页面中使用 , 看接下来的代码是否非常简单
<template>
<div>
<div ref="chartRef" style="width: 100%; height: 400px;"></div>
</div>
</template>
<script lang="ts" setup>
import {useECharts} from "@/hooks/useECharts";
const chartRef = ref<HTMLDivElement | null>(null);
const {options} = useECharts(chartRef, new LineOptionStrategy().getOptions());
</script>
<style lang="scss" scoped>
/* 样式 */
</style>
而修改更为简单,直接修改 options
就可以直接更改页面数据展示,我加上了以下代码
setTimeout(() => {
options.series[0].data = [210, 200, 200, 200, 200, 220, 280];
}, 1000)
3. API 中绑定 ECharts 数据显示格式
不知道大家看过我之前的文章没有,其中有一篇 让后端开发赞不绝口的 API 封装技巧:用 Axios 实现高效前端请求管理,我们就使用这种设计来定义一下我们的ECharts数据吧
const API_BASE = '/online_user';
const API_SUFFIXES = {
/** 在线用户实时流动 */
STREAM_SSE: '/user-activity-sse',
/** 省略其他接口 */
};
export class OnlineUserAPI {
/**
* 在线用户实时数据展示
*/
static STREAM_SSE = {
endpoint: (token: string) => {
return `${import.meta.env.VITE_APP_API_URL}${API_BASE}${API_SUFFIXES.STREAM_SSE}?Authorization=${token}`
},
permission: 'monitor:online-user:list',
chartOptions: (): EChartsOption => {
return {
title: {
text: '在线用户统计', // 图表标题
left: 'center' // 标题居中显示
},
xAxis: {
type: 'category',
data: ['35s', '30s', '25s', '20s', '15s', '10s', '5s'] // 可以根据需要修改为时间段或其他表示
},
yAxis: {
type: 'value'
},
series: [
{
data: ["0", "0", "0", "0", "0", "0", "0"], // 数据
type: 'line'
}
]
}
}
}
}
对于 STREAM_SSE图标格式 已经绑定在 STREAM_SSE接口上
我们查看对应API的时候,一眼就能看到对应API的所有配置,因为我们有ECharts Hooks 接下来的使用方式就是一样的了
<template>
<div>
<el-row :gutter="20">
<!-- 实时数据 -->
<el-col :lg="12" :md="12" :sm="12" :span="12" :xs="24">
<el-card>
<div ref="onlineChat" style="height: 300px;"></div>
</el-card>
</el-col>
<!-- 在线用户表格 -->
</el-row>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: "OnlineUser",
inheritAttrs: false,
});
const onlineChat = ref<HTMLDivElement | null>(null);
const {options} = useECharts(onlineChat, OnlineUserAPI.STREAM_SSE.chartOptions()) // ECharts 图表绘画
// 省略 SSE相关操作
</script>
让我们看看效果 在线用户的Echarts图表实时更新效果
五、结束语
谢谢大家看到这里,该Hooks为第一版,可能比较简陋,可以向我提出优化
源码地址 | 欢迎大家讨论以及start