不要再编写冗余的ECharts代码了,带你封装一个EChatrs Hook

7,458 阅读8分钟

一、介绍

在现在开发的项目中, 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);

结果在运行的时候出现了问题 :

image.png

解释一下 :

在 Vue 中,ref 和 reactive 都会为被包装的对象提供深度的响应式监听,可以会导致ECharts中的部分属性无法直接获取。这意味着,如果你用 ref 或 reactive 包装一个 ECharts 实例,Vue 将尝试监视该实例的所有内部属性的变化。但 ECharts 实例是一个非常复杂的对象,包含了大量与 DOM 操作、渲染引擎相关的属性和方法。对这些进行深度监听会带来额外的性能开销,而这些变化大多数情况下并不需要被监视。

使用 shallowRef 只会对 ECharts 实例的顶层进行响应式监听,而不会深入到其内部。这能避免不必要的计算和性能消耗,从而提高应用的效率。而 ECharts 实例本身并不需要频繁响应式地监视,因为大部分操作(如初始化、数据更新、调整大小)都是通过显式的 setOption 或 resize 方法完成的。通过这些方法,你可以手动触发图表的变化,因此不需要 Vue 的响应式系统来监视实例的每一个细节。

3. 优势

  1. 我们可以结合之前封装组件的方法与当前hooks结合,更加方便。
  2. 当有业务增加的时候,比如我们需要一个图片下载功能,可以直接在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
    };
}
  1. 其他优点仁者见仁智者见智 ( * ^ ▽ ^ * )

四、让我们快速上手一下这个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)

QQ录屏20240905134505.gif

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图表实时更新效果

QQ录屏20240905140238.gif

五、结束语

谢谢大家看到这里,该Hooks为第一版,可能比较简陋,可以向我提出优化

在线用户功能 SSE 文章

源码地址 | 欢迎大家讨论以及start