likes
comments
collection
share

React hooks 封装 Echarts5 组件

作者站长头像
站长
· 阅读数 73

按需引入 ECharts 图表和组件

在 Echarts5 中,引入了新的按需引入接口,因为它可以最大程度的利用打包工具 tree-shaking 的能力,并且可以有效解决命名空间冲突的问题而且防止了内部结构的暴露。

config 配置文件

由于依赖的调整,需要手动引入的地方比较多,我们可以单独放置在一个配置文件中:

// echarts.config.js
// * 需要哪些组件和配置,请在 import 时手动添加。
import * as echarts from 'echarts/core';
// 引入用到的图表
import { BarChart, PieChart } from 'echarts/charts';
// 引入提示框、数据集等组件
import { TitleComponent, TooltipComponent } from 'echarts/components';
// 引入标签自动布局、全局过渡动画等特性
import { LabelLayout } from 'echarts/features';
// 引入 Canvas 渲染器,必须
import { CanvasRenderer } from 'echarts/renderers';

// 注册必须的组件
echarts.use([
    BarChart,
    PieChart,
    TitleComponent,
    TooltipComponent,
    CanvasRenderer,
    LabelLayout
]);

export default echarts;

TypeScript 支持

需要使用 TS 的小伙伴,可以在 config 配置文件中添加类型。Echarts 提供了类型接口来组合出最小的 EChartsOption 类型,需要我们往其中手动添加类型即可。

// 类型相关
// 系列类型的定义后缀都为 SeriesOption
import type { BarSeriesOption, PieSeriesOption } from 'echarts/charts';
// 组件类型的定义后缀都为 ComponentOption
import type { TooltipComponentOption, TitleComponentOption } from 'echarts/components';
// 通过引入 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
import type { ComposeOption } from 'echarts/core';

// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
export type ECOption = ComposeOption<
    | BarSeriesOption
    | PieSeriesOption
    | TitleComponentOption
    | TooltipComponentOption
>;

基本组件分装

const EChart = (props, ref) => {
    const { options, style = {}} = props;
    const cDom = useRef(null);
    const cInstance = useRef(null);

    useEffect(() => {
        if (cDom.current) {
            // 校验 Dom 节点上是否已经挂载了 ECharts 实例,只有未挂载时才初始化
            cInstance.current = echarts.getInstanceByDom(cDom.current);
            if (!cInstance.current) {
                cInstance.current = echarts.init(cDom.current, null, {
                    renderer: 'svg',
                });
            }
            // 设置配置项
            options && cInstance.current.setOption(options);
        }

        return () => {
            // 容器被销毁之后,销毁实例,避免内存泄漏
            cInstance.current.dispose();
        };
    }, [cDom, options]);

    return <div id="echart" ref={cDom} style={{ width: '100%', height: '80%', ...style }} />;
};

export default EChart;

cDom:dom 容器,就可以不用 document.getElementById('echart')写法了;

cInstance:保存 dom 容器上的 Echarts 实例;

窗口自适应

当页面窗口发生变化的时候,我们希望图表可以自动调整大小,因此还需要添加一个 resize 监听事件。

// 窗口自适应并开启过渡动画
const resize = () => {
    if(cInstance.current) {
        cInstance.current.resize({
            animation: { duration: 300 },
        });
    }
};

// 监听窗口大小
useEffect(() => {
    window.addEventListener('resize', debounceResize);

    return () => {
        window.removeEventListener('resize', debounceResize);
    };
}, []);

额外监听宽高

如果你有可能会手动修改宽度和高度,还可以额外监听它们。

const EChart = (props) => {
    const { style } = props;
    // 监听高度变化
    useLayoutEffect(() => {
        resize();
    }, [style.width, style.height]);
}

自适应防抖优化

如果不想频繁触发自适应事件,我们可以加个防抖优化:

import debounce from 'lodash-es/debounce';

// 自适应防抖优化
const debounceResize = debounce(resize, 500);

并使用 debounceResize 替换 resize

绑定鼠标点击事件

ECharts 提供了很多的events,且主要通过 on 方法添加事件处理函数。这里,我们添加鼠标事件中的点击事件,将其绑定在接口中。

const EChart = (props) => {
    const { onClick } = props;
    
    useEffect(() => {
        if (cDom.current) {
            // 略......
            // 添加绑定鼠标点击事件
            cInstance.current.on('click', event => {
                if (event && onClick) onClick(event);
            });
        }
    }, [cDom, options]);
}

展示 loading 动画

Echarts 提供了内置的加载动画效果,我们可以在加载数据前手动调用该接口显示加载动画,通过 showLoadinghideloading 来控制。

const EChart = (props) => {
    const { loading = false } = props;
    
    // 展示 loading 动画
    useEffect(() => {
        loading
            ? cInstance.current.showLoading()
            : cInstance.current.hideLoading();
    }, [loading]);
}

暴露实例方法

对父组件暴露获取 ECharts 实例的方法,让父组件可直接通过实例调用原生函数。

const EChart = (props, ref) => {
    // 略......
    // 获取实例
    const getInstance = () => cInstance.current;
    // 暴露方法
    useImperativeHandle(ref, () => ({ getInstance }));
}
export default React.forwardRef(EChart);

Memorized 缓存优化

最后,作为一个全局通用组件,我们可以使用 React.memo 做一个缓存优化,让组件在 props 不变的情况下,跳过 re-render。

export default React.memo(React.forwardRef(EChart));

完整代码

  • echarts.config.js
import * as echarts from 'echarts/core';
// 引入用到的图表
import { BarChart, PieChart } from 'echarts/charts';
// 引入提示框、数据集等组件
import { TitleComponent, TooltipComponent } from 'echarts/components';
// 引入标签自动布局、全局过渡动画等特性
import { LabelLayout } from 'echarts/features';
// 引入 Canvas 渲染器,必须
import { CanvasRenderer } from 'echarts/renderers';

// 类型相关
// 系列类型的定义后缀都为 SeriesOption
import type { BarSeriesOption, PieSeriesOption } from 'echarts/charts';
// 组件类型的定义后缀都为 ComponentOption
import type { TooltipComponentOption, TitleComponentOption } from 'echarts/components';
// 通过引入 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
import type { ComposeOption } from 'echarts/core';

// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
export type ECOption = ComposeOption<
    | BarSeriesOption
    | PieSeriesOption
    | TitleComponentOption
    | TooltipComponentOption
>;

// 注册必须的组件
echarts.use([
    BarChart,
    PieChart,
    TitleComponent,
    TooltipComponent,
    CanvasRenderer,
    LabelLayout
]);

export default echarts;

  • BaseChart.jsx
import React, { useRef, useEffect, useLayoutEffect, useImperativeHandle } from 'react';
import debounce from 'lodash-es/debounce';
import echarts from './echarts.config';

const EChart = (props, ref) => {
    const {
        options,
        loading = false, // 受控
        onClick,
        style = {},
    } = props;
    const cDom = useRef(null);
    const cInstance = useRef(null);

    // 获取实例
    const getInstance = () => cInstance.current;

    // 窗口自适应并开启过渡动画
    const resize = () => {
        cInstance.current &&
            cInstance.current.resize({
                animation: { duration: 300 },
            });
    };

    // 自适应防抖优化
    const debounceResize = debounce(resize, 500);

    useEffect(() => {
        if (cDom.current) {
            // 校验 Dom 节点上是否已经挂载了 ECharts 实例,只有未挂载时才初始化
            cInstance.current = echarts.getInstanceByDom(cDom.current);
            if (!cInstance.current) {
                cInstance.current = echarts.init(cDom.current, null, {
                    renderer: 'svg',
                });
            }

            // 绑定鼠标点击事件
            cInstance.current.on('click', event => {
                if (event && onClick) onClick(event);
            });

            options && cInstance.current.setOption(options);
        }

        return () => {
            cInstance.current.dispose(); // 容器被销毁之后,销毁实例,避免内存泄漏
        };
    }, [cDom, options]);

    // 监听高度变化
    useLayoutEffect(() => {
        resize();
    }, [style.width, style.height]);

    // 监听窗口大小
    useEffect(() => {
        window.addEventListener('resize', debounceResize);

        return () => {
            window.removeEventListener('resize', debounceResize);
        };
    }, [options]);

    // 展示 loading 动画
    useEffect(() => {
        loading ? cInstance.current.showLoading() : cInstance.current.hideLoading();
    }, [loading]);

    // 对父组件暴露获取ECharts实例的方法,可直接通过实例调用原生函数
    useImperativeHandle(ref, () => ({ getInstance }));

    return <div id="echart" ref={cDom} style={{ width: '100%', height: '80%', ...style }} />;
};

export default React.memo(React.forwardRef(EChart));

参考资料

转载自:https://juejin.cn/post/7235603140261937209
评论
请登录