前言
本篇文章主要讲解如何使用组合式函数(Composables)来封装 Echarts,提供一套可复用、易维护的图表解决方案
在这里你能够学到 Echarts 封装的思路与最佳实践,理解 Echarts 的特性与使用技巧
本文也是《通俗易懂的中后台系统建设指南》系列的第六篇文章,该系列旨在告诉你如何来构建一个优秀的中后台管理系统
什么是 Echarts
Echarts 是一个基于 JavaScript 的开源可视化图表库,具有丰富的图表类型和强大的交互能力,凭借其优秀的性能和灵活的配置,成为前端可视化主流方案
Echarts 的使用场景很多,比如:
- 数据监控和实时数据展示,比如物联网、实时数据监控、金融
- 业务数据可视化,比如电商销售、商品价格等
- 地理信息可视化,比如物流跟踪、商圈分析、气象预报
- 报表数据直观可视化,比如财务报表
功能列表
在本文中,我们会实现这些功能:
- 自适应不同屏幕尺寸变化,结合防抖机制提升性能,确保流畅体验
- 灵活配置图表样式与行为,满足多样化的使用场景
- 支持明暗主题动态切换,提供良好的视觉体验
- Loading 状态管理,优化加载体验
- 支持图表交互事件
- 支持图表导出与截图功能
本文的基础开发环境
技术栈:Vue3 + TS + Vite
包管理器:pnpm
组合式函数工具库:VueUse
可视化图表库:Echarts
原子化 CSS:tailwindcss
在项目中引入 Echarts
pnpm add echarts --save
引入 Echarts 分为全量引入和按需引入,这里选择按需引入,参阅 NPM 安装 ECharts
我这里放在 plugins
文件夹下,且新建一个 echarts.ts
文件,写入以下配置:
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
import * as echarts from 'echarts/core';
// 引入柱状图图表,图表后缀都为 Chart
import { BarChart } from 'echarts/charts';
// 引入标题,提示框,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
import {
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent
} from 'echarts/components';
// 标签自动布局、全局过渡动画等特性
import { LabelLayout, UniversalTransition } from 'echarts/features';
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { CanvasRenderer } from 'echarts/renderers';
// 注册必须的组件
echarts.use([
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
BarChart,
LabelLayout,
UniversalTransition,
CanvasRenderer
]);
export { echarts }
这是 Echarts 的起步配置,下面我们将通过组合式函数来封装 Echarts
开始
在 src/hooks
目录下新建一个 use-echarts.ts
文件,这个文件表示使用组合式函数来封装图表逻辑(在 React 里被称为 Hooks)
import { Ref } from 'vue';
export const useEcharts = (dom: Ref<HTMLDivElement | HTMLCanvasElement | null>, config = {}) => {};
dom
:要绑定的 DOM 元素config
:接收一些外部传入的配置项(在下文实现中用到)
图表初始化
创建一个 initChart
函数,用于初始化图表
import { Ref, onMounted } from 'vue';
import { echarts } from '@/plugins/echarts';
import type { EChartsInitOpts } from 'echarts';
interface ConfigProps {
/**
* init函数基本配置
* @see https://echarts.apache.org/zh/api.html#echarts.init
*/
echartsInitOpts?: EChartsInitOpts;
}
export const useEcharts = (
dom: Ref<HTMLDivElement | HTMLCanvasElement | null>,
config: ConfigProps = {},
) => {
const { echartsInitOpts } = config;
/** 图表实例 */
let chartInstance: NullType<echarts.ECharts> = null;
/** 图表初始化 */
const initChart = () => {
if (!dom.value || echarts.getInstanceByDom(dom.value)) return;
chartInstance = echarts.init(dom.value, null, echartsInitOpts);
};
/** 获取图表实例 */
const getChartInstance = () => chartInstance;
onMounted(() => {
initChart();
});
return {
getChartInstance,
};
};
注意,这里用到了NullType
,这是一个简单的泛型,type NullType<T> = T | null;
在上面的代码逻辑里,核心代码是 echarts.init
echarts.init
函数接受三个参数,第一个必填参数是要绑定的 dom
节点,第二个可选参数是主题 theme
,这里先设置为null,第三个可选参数是一些配置项,比如指定宽、高度,指定语言 locale
等,然后会返回一个 ECharts 实例,参阅 echarts.init
最后在组件挂载完成后(onMounted)执行 initChart
函数
图表的销毁
上述我们写了一个初始化函数,这里我们来写一下相对的销毁逻辑
chartInstance
实例上挂载了一个 dispose
函数,这个方法可用于销毁实例,参阅 echarts.dispose
import { onUnmounted } from 'vue';
/** 图表销毁 */
const destroyChart = () => {
if (!chartInstance) return;
chartInstance.dispose();
chartInstance = null;
};
// 组件实例被卸载之后
onUnmounted(() => {
destroyChart();
});
上面这段代码的作用主要是在容器被销毁后,通过执行 destroyChart
函数来释放资源,避免内存泄漏
渲染图表与配置options
chartInstance
实例上挂载了一个 setOption
函数,用于设置图表实例的配置项以及数据
我们的总体思路是:导出一个函数,由外部传入 options 数据进行图表渲染
所以,创建一个 renderChart
函数,表示这个函数用于将数据渲染成可视化图表
import type { EChartsCoreOption, EChartsInitOpts, SetOptionOpts } from 'echarts';
/**
* 图表渲染
* @param options 图表数据集
* @param opts 图表配置项
*/
const renderChart = (options: EChartsCoreOption, opts: SetOptionOpts = { notMerge: true }) => {
const finalOptions = { ...options, backgroundColor: 'transparent' };
chartInstance.setOption(finalOptions, opts);
};
在上述代码中,renderChart
函数接收两个参数,第一个参数是图表的数据集,第二个可选参数是针对数据项的设置,有两种配置,参阅 这里,在此处代码示例中,我们约定接收一个对象,它的类型配置如下:
(option: Object, opts?: {
notMerge?: boolean;// 是否不跟之前设置的 `option` 进行合并
replaceMerge?: string | string[];// 用户可以在这里指定一个或多个组件
lazyUpdate?: boolean;// 在设置完 `option` 后是否不立即更新图表
})
响应式图表尺寸与防抖
响应式尺寸
在 Echarts 官网-响应式容器大小变化章节的介绍中,我们可以看到主要是通过监听页面的 resize
事件获取浏览器大小改变的事件,然后调用 echartsInstance.resize
改变图表的大小,文章中也提到一种更细粒度的方法是通过 ResizeObserver
API 来监听尺寸变化
所以,这里我们可以借助 Vue
生态中的工具型组合式函数集合 VueUse
中的 useResizeObserver
来实现
注意,在这一步需要确保项目中安装了依赖VueUse
,如果没有,使用命令pnpm add @vueuse/core
安装
创建一个 resize
函数,写入以下内容:
import { Ref, onMounted, onUnmounted } from 'vue';
import { echarts } from '@/plugins/echarts';
import { useResizeObserver } from '@vueuse/core';
import type { UseResizeObserverReturn } from '@vueuse/core';
import type { EChartsCoreOption, EChartsInitOpts, SetOptionOpts } from 'echarts';
interface ConfigProps {
/**
* 是否开启过渡动画
* @default true
*/
animation?: boolean;
/**
* 过渡动画持续时间(ms)
* @default 300
*/
animationDuration?: number;
/**
* 是否自动调整大小
* @default true
*/
autoResize?: boolean;
}
const DEFAULT_CONFIG: ConfigProps = {
animation: true,
animationDuration: 300,
autoResize: true,
};
export const useEcharts = (
dom: Ref<HTMLDivElement | HTMLCanvasElement | null>,
config: ConfigProps,
) => {
const { animation, animationDuration, autoResize } = { ...DEFAULT_CONFIG, ...config };
/** 图表实例 */
let chartInstance: NullType<echarts.ECharts> = null;
/** 图表尺寸变化监听 */
let resizeObserver: NullType<UseResizeObserverReturn> = null;
//...
/** 图表销毁 */
const destroyChart = () => {
if (autoResize && resizeObserver) {
resizeObserver.stop();
resizeObserver = null;
}
//...
};
/** 调整图表尺寸 */
const resize = () => {
if (!chartInstance) return;
chartInstance.resize({
animation: {
duration: animation ? animationDuration : 0,
},
});
};
onMounted(() => {
//...
if (autoResize) {
resizeObserver = useResizeObserver(dom, resize);
}
});
// 组件实例被卸载之后
onUnmounted(() => {
destroyChart();
});
return {
renderChart,
};
};
在上面的代码中,着重关注这几点:
resizeObserver
:一个可为空的UseResizeObserverReturn
类型对象,用于监听指定 DOM 元素的尺寸变化并在变化时执行回调函数config
、DEFAULT_CONFIG
:参数配置项及默认配置chartInstance.resize
:核心方法,调整图表尺寸destroyChart
函数上使用resizeObserver
方法stop
停止监听尺寸
防抖
在上述代码中,我们实现了响应式的尺寸功能。然而,当尺寸频繁变化时,可能会对性能造成较大压力。为提升性能,我们可以进行进一步优化,加入防抖,这里借助 VueUse
的 useDebounceFn
来实现
防抖:在事件持续触发时,不会立即执行,而是会等待一段时间后执行。若在等待时间内事件再次触发,则重新计时。只有在事件停止触发后的指定时间内,没有新的事件触发时,才会执行事件
import { Ref, onMounted, onUnmounted } from 'vue';
import { echarts } from '@/plugins/echarts';
import { useDebounceFn, useResizeObserver } from '@vueuse/core';
import type { UseResizeObserverReturn } from '@vueuse/core';
import type { EChartsCoreOption, EChartsInitOpts, SetOptionOpts } from 'echarts';
interface ConfigProps {
//...
/**
* 防抖时间(ms)
* @default 300
*/
resizeDebounceWait: number;
/**
* 最大防抖时间(ms)
* @default 500
*/
maxResizeDebounceWait: number;
}
const DEFAULT_CONFIG: ConfigProps = {
animation: true,
animationDuration: 300,
autoResize: true,
resizeDebounceWait: 300,
maxResizeDebounceWait: 500,
};
export const useEcharts = (
dom: Ref<HTMLDivElement | HTMLCanvasElement | null>,
config: ConfigProps,
) => {
const {
echartsInitOpts,
animation,
animationDuration,
autoResize,
resizeDebounceWait,
maxResizeDebounceWait,
} = { ...DEFAULT_CONFIG, ...config };
//...
/** 调整图表尺寸 */
const resize = () => {
//...
};
/** 防抖处理的resize */
const resizeDebounce = useDebounceFn(resize, resizeDebounceWait, {
maxWait: maxResizeDebounceWait,
});
onMounted(() => {
//...
if (autoResize) {
resizeObserver = useResizeObserver(dom, resizeDebounce);
}
});
};
这里你需要关注这几点:
useDebounceFn
:防抖函数,由VueUse
提供config
参数,DEFAULT_CONFIG
默认配置中新加入了resizeDebounceWait
防抖时间、maxResizeDebounceWait
防抖最大时间
到这一步,其实基础的图表已经能够实现,我们来测试一下效果
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useEcharts } from '@/hooks/use-echarts';
import { userVisitOption } from './data';
const userInstance = ref<NullType<HTMLDivElement>>(null);
const { renderChart } = useEcharts(userInstance);
onMounted(() => {
renderChart(userVisitOption);
});
</script>
<template>
<!--其他内容略...-->
<div ref="userInstance" class="w-full h-72" />
</template>
userVisitOption
表示你的图表数据集
图表的主题模式切换
一般来说,主题模式分为明亮模式和暗黑模式,Echarts 除了一贯的默认主题外,还支持暗黑模式
图表的主题切换需要通过 echarts.init
的第二个参数进行配置,我们在上述的图表初始化章节中简单提了一句,现在来完善它
在类型定义中加入 themeMode
,它的联合类型中:
dark
代表 Echarts 内置的暗黑模式string
类型表示你可以传入注册的主题名称null
表示为默认的明亮模式
interface ConfigProps {
//...略
/**
* 主题模式
*/
themeMode?: 'dark' | string | null;
}
import { computed, watch } from 'vue';
import { useDark } from '@vueuse/core';
export const useEcharts = (
dom: Ref<HTMLDivElement | HTMLCanvasElement | null>,
config: ConfigProps,
) => {
const {
//...
themeMode,
} = { ...DEFAULT_CONFIG, ...config };
const isDark = useDark();
/** 当前主题 */
const currentTheme = computed(() => {
// 如果设置了自定义主题模式,优先使用
if (themeMode || isNull(themeMode)) {
return themeMode;
}
// 否则根据系统主题自动切换
return isDark.value ? 'dark' : null;
});
/** 图表初始化 */
const initChart = async () => {
//...
chartInstance = echarts.init(dom.value, currentTheme.value, echartsInitOpts);
};
// 监听主题变化,自动重新初始化图表
watch(currentTheme, async () => {
if (!chartInstance) return;
destroyChart();
await initChart();
if (chartOptions.value) {
renderChart(chartOptions.value);
}
});
};
你需要关注的几个点:
最终效果是这样的:
阶段总结
上面几个章节中,我们实现了一个图表基本的功能,到这一步,图表已经是可以拿来即用的状态了
在全面阅读 Echarts 的文档时,发现其 Api 极其丰富,可实现的功能也很多,下面会介绍一些丰富性的内容
loading状态
chartInstance
实例上挂载了2个方法,用于显示/隐藏 Loading 效果,分别是 showLo ading和 hideLoading
正如官网所说的:可以在加载数据前手动调用该接口显示加载动画,在数据加载完成后调用 hideLoading 隐藏加载动画
/** 图表实例 */
let chartInstance: NullType<echarts.ECharts> = null;
/** Loading 状态控制 */
const toggleLoading = (show: boolean) => {
if (!chartInstance) return;
show ? chartInstance.showLoading('default') : chartInstance.hideLoading();
};
/** 图表初始化 */
const initChart = async () => {
//...
toggleLoading(true);
};
/**
* 图表渲染
* @param options 图表数据
* @param opts 图表配置项
*/
const renderChart = (options: EChartsCoreOption, opts: SetOptionOpts = { notMerge: true }) => {
//...
toggleLoading(false);
};
return {
toggleLoading,
};
事件支持
Echarts 的事件与行为文章中向我们介绍了事件相关的内容,可以通过用户操作触发事件,监听事件以触发回调函数进行自定义需求处理
同时,在上述的初始化篇章里,我们有导出一个 getChartInstance
函数,这个函数用于返回一个图表实例,所以,我们可以这样做
此部分不涉及到 echarts
封装代码,只是使用示例
import { onMounted } from 'vue';
import { userVisitOption } from './data';
const { renderChart, getChartInstance } = useEchartss(userInstance);
onMounted(() => {
renderChart(userVisitOption);
getChartInstance()?.on('click', (params: any) => {
console.log('点我查看信息:', params);
});
});
导出图片
chartInstance
实例上挂载了一个 getDataURL
方法,该方法返回一个 base64 的 URL
首先先来定义配置项的类型与默认配置
interface DataURLOptions {
/**
* 导出的格式,可选 png, jpg, svg
* @default png
*/
type?: 'png' | 'jpeg' | 'svg';
/**
* 导出的图片分辨率比例
* @default 1
*/
pixelRatio?: number;
/**
* 导出的图片背景色
* @default #fff
*/
backgroundColor?: Color;
/**
* 导出的图片排除的列表
*/
excludeComponents?: string[];
}
/** 导出文件默认配置 */
const DEFAULT_EXPORT_OPTIONS: DataURLOptions = {
type: 'png',
pixelRatio: 1,
backgroundColor: '#fff',
excludeComponents: [],
};
然后写一个 downloadImage
方法。方法很简单,base64 数据通过 downloadFile
方法内 JS 创建了 a 标签进行点击下载
/** 下载图表文件 */
const downloadImage = (fileName: string, options?: DataURLOptions) => {
if (!chartInstance) return;
const baseOptions: DataURLOptions = {
...DEFAULT_EXPORT_OPTIONS,
...options,
};
const dataURL = chartInstance.getDataURL(baseOptions);
const finalFileName = /^[a-z0-9]+$/i.test(fileName.trim())
? fileName
: `${fileName.trim()}.${baseOptions.type}`;
downloadFile(dataURL, finalFileName);
};
return {
//...略
downloadImage,
};
文中的 downloadFile
是一个工具函数,这里讲它超出了本文的内容,如果你需要的话,可以在这里找到源码
完整代码
本文中完整的示例代码可以在这里找到:use-echarts
参考资料
不要再编写冗余的ECharts代码了,带你封装一个EChatrs Hook
了解更多
此系列实战项目:vue-clean-admin
专栏往期回顾:
- 收下这份 Vue + TS + Vite 中后台系统搭建指南,从此不再害怕建项目
- 中后台开发必修课:Vue 项目中 Pinia 与 Router 完全攻略
- 用了这些 Vite 配置技巧,同事都以为我开挂了
- 受够了团队代码风格不统一?7千字教你从零搭建代码规范体系
- 开发者必看!在团队中我是这样实现 Git 提交规范化的
交流讨论
文章如有错误或需要改进之处,欢迎指正
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。