头图

前言

本篇文章主要讲解如何使用组合式函数(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 元素的尺寸变化并在变化时执行回调函数
  • configDEFAULT_CONFIG:参数配置项及默认配置
  • chartInstance.resize:核心方法,调整图表尺寸
  • destroyChart 函数上使用 resizeObserver 方法 stop 停止监听尺寸

防抖

在上述代码中,我们实现了响应式的尺寸功能。然而,当尺寸频繁变化时,可能会对性能造成较大压力。为提升性能,我们可以进行进一步优化,加入防抖,这里借助 VueUseuseDebounceFn来实现

防抖:在事件持续触发时,不会立即执行,而是会等待一段时间后执行。若在等待时间内事件再次触发,则重新计时。只有在事件停止触发后的指定时间内,没有新的事件触发时,才会执行事件
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 表示你的图表数据集

Kapture 2024-11-09 at 14 50 13

图表的主题模式切换

一般来说,主题模式分为明亮模式和暗黑模式,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);
    }
  });
  
};

你需要关注的几个点:

  • useDark:由 VueUse 提供的组合式函数,响应式暗黑模式状态,参阅 useDark
  • 监听 currentTheme 变量:当变量改变时,先销毁图表实例,再重新初始化和渲染数据集

最终效果是这样的:

Kapture 2024-11-13 at 17 54 36

阶段总结

上面几个章节中,我们实现了一个图表基本的功能,到这一步,图表已经是可以拿来即用的状态了

在全面阅读 Echarts 的文档时,发现其 Api 极其丰富,可实现的功能也很多,下面会介绍一些丰富性的内容

loading状态

chartInstance 实例上挂载了2个方法,用于显示/隐藏 Loading 效果,分别是 showLo adinghideLoading

正如官网所说的:可以在加载数据前手动调用该接口显示加载动画,在数据加载完成后调用 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

了解更多

系列专栏地址:GitHub | 掘金专栏 | 思否专栏

此系列实战项目:vue-clean-admin

专栏往期回顾:

  1. 收下这份 Vue + TS + Vite 中后台系统搭建指南,从此不再害怕建项目
  2. 中后台开发必修课:Vue 项目中 Pinia 与 Router 完全攻略
  3. 用了这些 Vite 配置技巧,同事都以为我开挂了
  4. 受够了团队代码风格不统一?7千字教你从零搭建代码规范体系
  5. 开发者必看!在团队中我是这样实现 Git 提交规范化的

交流讨论

文章如有错误或需要改进之处,欢迎指正


十五
1 声望1 粉丝