React Hooks 日常开发中可能会遇到多个Echarts数据图表需要,为了便于复用和治理,开发一个通用型公共图表组件是十分有必要的。

首先定义用于图表的类型文件IChart.ts:

import  {  CSSProperties } from 'react';import * as echarts from 'echarts/core';import {    // 系列类型的定义后缀都为 SeriesOption    BarSeriesOption,    LineSeriesOption} from 'echarts/charts';import {    TitleComponentOption,    TooltipComponentOption,    GridComponentOption,    DatasetComponentOption,} from 'echarts/components';// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型export type IECOption = echarts.ComposeOption<  | BarSeriesOption  | LineSeriesOption  | TitleComponentOption  | TooltipComponentOption  | GridComponentOption  | DatasetComponentOption>;export interface IOnEvents{    [index: string]: (param: unknown, instance: echarts.ECharts) => void;}type RendererType = 'canvas' | 'svg';interface IEChartsInitOpts {    readonly locale?: string ;    readonly renderer?: RendererType;    readonly devicePixelRatio?: number;    readonly useDirtyRect?: boolean;    readonly useCoarsePointer?: boolean;    readonly pointerSize?: number;    readonly ssr?: boolean;    readonly width?: number | string;    readonly height?: number | string;}export interface IChartProps  {    readonly option: IECOption;    readonly loading?: boolean;    readonly onEvents?: IOnEvents;    readonly onChartReady?:  (echartsInstance: echarts.ECharts) => void;    readonly style?: CSSProperties;    readonly className?: string;    readonly theme?: string | object;    readonly opts?: IEChartsInitOpts;}

IChartPropsChart组件的入参。

接下来实现图表组件:
elementResizeDetectorMaker是用于依据父元素变动而后动静调整图表大小的性能。
留神echarts.use图表组件注册局部是要按需引入/注册。
props 为一些比拟罕用的图表配置。
Chart.tsx

import React, { useEffect, useRef, useCallback } from 'react';import * as echarts from 'echarts/core';import elementResizeDetectorMaker from 'element-resize-detector';import {    BarChart,    PieChart,    LineChart,} from 'echarts/charts';import {    TitleComponent,    TooltipComponent,    GridComponent,    // 数据集组件    DatasetComponent,    LegendComponent,    // 内置数据转换器组件 (filter, sort)    TransformComponent} from 'echarts/components';import { LabelLayout, UniversalTransition } from 'echarts/features';import { CanvasRenderer } from 'echarts/renderers';import { IChartProps } from './IChart';// 注册必须的组件echarts.use([    TitleComponent,    TooltipComponent,    GridComponent,    DatasetComponent,    TransformComponent,    BarChart,    LegendComponent,    PieChart,    LineChart,    LabelLayout,    UniversalTransition,    CanvasRenderer]);const bindEvents: (instance: echarts.ECharts, events: IChartProps['onEvents']) => void = (instance, events) => {    for (const eventName in events) {        if (Object.prototype.hasOwnProperty.call(events, eventName)) {            const func = events[eventName];            instance.on(eventName, (param) => {                func(param, instance);            });        }    }};const Chart: React.FC<IChartProps> = (props) => {    const option = props.option;    const loading = props.loading;    const onEvents = props.onEvents;    const onChartReady = props.onChartReady;    const style = props.style;    const theme = props.theme;    const opts = props.opts;    const tableSelectRef = useRef<HTMLDivElement>();    const getEchartsInstance: () => echarts.ECharts = () => {        return echarts.getInstanceByDom(tableSelectRef.current!)!;    };    const dispose: () => void = () => {        if (tableSelectRef.current) {            echarts.dispose(tableSelectRef.current);        }    };    const newChart: () => Promise<void> = useCallback(async () => {        return new Promise((resolve, reject) => {            try {                const myChart = echarts.init(tableSelectRef.current!, theme, opts);                myChart.on('finished', () => {                    resolve();                });            }            catch (error) {                reject(error);            }        });    }, [opts, theme]);    useEffect(() => {        dispose();        newChart().then(() => {            // 'finished'            const myChart = getEchartsInstance();            bindEvents(myChart, onEvents);            onChartReady?.(myChart);            myChart.clear();            myChart.setOption(option, true);            const erd = elementResizeDetectorMaker();            if (tableSelectRef.current) {                erd.listenTo(tableSelectRef.current, () => {                    // 父元素大小resize                    myChart.resize({                        width: 'auto',                        height: 'auto',                    });                });            }        }).catch(err => {            console.log(err);            dispose();        });        return () => {            dispose();        };        //option扭转不进行重渲染        // eslint-disable-next-line react-hooks/exhaustive-deps    }, [theme, opts, newChart, onEvents, onChartReady]);    useEffect(() => { // option 扭转        getEchartsInstance().clear();        getEchartsInstance().setOption(option, true);    }, [option]);    useEffect(() => { // loading 扭转        if (loading) {            getEchartsInstance().showLoading();        }        else {            getEchartsInstance().hideLoading();        }    }, [loading]);    return (        <div            className={`echarts ${props.className ?? ''}`}            style={{ height: '300px', ...style }}            ref={(node) => {                tableSelectRef.current = node!;            }}        >        </div>    );};export default Chart;

第一个useEffect是创立图表的实例的init实现后(finished),才退出配置项,并执行bindEvents绑定对应的罕用官网事件,最初在useEffectreturn中及时销毁以后图表。

留神第一个useEffect最初的依赖数组中有加上了// eslint-disable-next-line react-hooks/exhaustive-deps表明成心疏忽了option变动的eslint提醒,因为这里是第一次的图表实例初始化,并不需要每次下层的option变动都要从新执行,所以option变动从新在第二个useEffect中再次setOption就行

应用例子:
传入option配置项,当后续option中再次变动时触发Chart组件从新更新。
CostRateRatio.tsx

import React, { useState, useMemo, useCallback, useEffect } from 'react';import { Card } from 'antd';import Chart from '@/basic/Chart';import { IECOption } from '@/basic/Chart/IChart';import * as api from '@/api';interface IProps {    queryParams: any;}const CostRateRatio: React.FC<IProps> = props => {    const [data, setData] = useState<any>({        'current': []    });    const [loading, setLoading] = useState(false);    const fetchLineChart = useCallback(async () => {        try {            setLoading(true);            const res = await api.getLineChart(props.queryParams);            setData(res.data);        }        catch (error) {            console.error(error);        }        finally {            setLoading(false);        }    }, [props.queryParams]);    useEffect(() => {        fetchLineChart().catch(console.error);    }, [fetchLineChart]);    const option = useMemo(() => {        return {            title: { text: '折线图' },            tooltip: { trigger: 'axis' },            xAxis: {                type: 'category',                axisPointer: {                    show: true,                }            },            legend: {                type: 'scroll',                bottom: 0            },            yAxis: {                type: 'value'            },            dataset: [{                dimensions: ['abscissa', 'tariff', 'advertisingCost', 'storageCost', 'refund'],                source: data.current            },            {                dimensions: ['abscissa',   'tariff', 'advertisingCost',  'storageCost', 'refund'],                source: data.contrastive            }],            series: [                {                    name: '推广费',                    type: 'line',                },                {                    name: 'test',                    type: 'line',                },                {                    name: 'abc费',                    type: 'line',                },                {                    name: '退货费',                    type: 'line',                },            ]        };    }, [data]);    return (        <div style={{ width: '50%' }}>            <Card size="small">                <Chart                    option={option as IECOption}                    loading={loading}                    style={{ height: 300, width: '100%' }}                >                </Chart>            </Card>        </div>    );};export default CostRateRatio;