React+TS前台项目实战(二十六)-- 高性能可配置Echarts图表组件封装

文章目录

  • 前言
  • CommonChart组件
    • [1. 功能分析](#1. 功能分析)
    • [2. 代码+详细注释](#2. 代码+详细注释)
    • [3. 使用到的全局hook代码](#3. 使用到的全局hook代码)
    • [4. 使用方式](#4. 使用方式)
    • [5. 效果展示](#5. 效果展示)
  • 总结

前言

Echarts图表在项目中经常用到,然而,重复编写初始化,更新,以及清除实例等动作对于开发人员来说是一种浪费时间和精力。因此,在这篇文章中,将封装一个 "高性能可配置Echarts组件" ,简化开发的工作流程,提高数据可视化的效率和质量。

CommonChart组件

1. 功能分析

(1)可以渲染多种类型的图表,包括折线图、柱状图、饼图、地图和散点图

(2)通过传入的 option 属性,配置图表的各种参数和样式

(4)通过传入的 onClick 属性,处理图表元素的点击事件

(5)通过传入的 notMerge 属性,控制是否合并图表配置

(6)通过传入的 lazyUpdate 属性,控制是否懒渲染图表

(7)通过传入的 style 属性,设置图表容器的样式

(8)通过传入的 className 属性,自定义图表容器的额外类名

(9)通过监听窗口大小变化,自动调整图表的大小

(10)使用 usePrevious、useWindowResize 和 useEffect 等钩子来提高组件性能并避免不必要的渲染

2. 代码+详细注释

c 复制代码
// @/components/Echarts/commom/index.tsx
import { useRef, useEffect, CSSProperties } from "react";
// 引入 Echarts 的各种图表组件和组件配置,后续备用
import "echarts/lib/chart/line"; // 折线图
import "echarts/lib/chart/bar"; // 柱状图
import "echarts/lib/chart/pie"; // 饼图
import "echarts/lib/chart/map"; // 地图
import "echarts/lib/chart/scatter"; // 散点图
import "echarts/lib/component/tooltip"; // 提示框组件
import "echarts/lib/component/title"; // 标题组件
import "echarts/lib/component/legend"; // 图例组件
import "echarts/lib/component/markLine"; // 标线组件
import "echarts/lib/component/dataZoom"; // 数据区域缩放组件
import "echarts/lib/component/brush"; // 刷选组件
// 引入 Echarts 的类型声明
import * as echarts from "echarts";
import { ECharts, EChartOption } from "echarts";
// 引入自定义的钩子函数和公共函数
import { useWindowResize, usePrevious } from "@/hooks";
import { isDeepEqual } from "@/utils";
/**
 * 公共 Echarts 业务灵巧组件,可在项目中重复使用
 *
 * @param {Object} props - 组件属性
 * @param {EChartOption} props.option - Echarts 配置项
 * @param {Function} [props.onClick] - 点击事件处理函数
 * @param {boolean} [props.notMerge=false] - 是否不合并数据
 * @param {boolean} [props.lazyUpdate=false] - 是否懒渲染
 * @param {CSSProperties} [props.style] - 组件样式
 * @param {string} [props.className] - 组件类名
 * @returns {JSX.Element} - React 组件
 */
type Props = {
  option: EChartOption;
  onClick?: (param: echarts.CallbackDataParams) => void;
  notMerge?: boolean;
  lazyUpdate?: boolean;
  style?: CSSProperties;
  className?: string;
};
const CommonChart = (props: Props) => {
  // 解构属性,并设置默认值
  const {
    option,
    onClick, // 点击事件处理函数
    notMerge = false, // 是否不合并数据,默认为 false
    lazyUpdate = false, // 是否懒渲染,默认为 false
    style, // 组件样式
    className = "", // 组件类名,默认为空字符串
  } = props;
  // 创建 ref 来引用 div 元素,并初始化 chartInstanceRef 为 null
  const chartRef = useRef<HTMLDivElement>(null);
  const chartInstanceRef = useRef<ECharts | null>(null);
  // 使用 usePrevious 钩子函数来记录上一次的 option 和 onClick 值
  const prevOption = usePrevious(option);
  const prevClickEvent = usePrevious(onClick);

  useEffect(() => {
    // 定义一个变量来存储图表实例
    let chartInstance: ECharts | null = null;
    if (chartRef.current) {
      // 如果图表实例不存在,则初始化
      if (!chartInstanceRef.current) {
        const hasRenderInstance = echarts.getInstanceByDom(chartRef.current);
        if (hasRenderInstance) {
          hasRenderInstance.dispose();
        }
        chartInstanceRef.current = echarts.init(chartRef.current);
      }
      // 暂存当前的图表实例
      chartInstance = chartInstanceRef.current;
      // 如果 option 或 onClick 值发生变化,则重新渲染
      try {
        if (!isDeepEqual(prevOption, option, ["formatter"])) {
          chartInstance.setOption(option, { notMerge, lazyUpdate });
        }
        if (onClick && typeof onClick === "function" && onClick !== prevClickEvent) {
          chartInstance.on("click", onClick);
        }
      } catch (error) {
        chartInstance && chartInstance.dispose();
      }
    }
  }, [option, onClick, notMerge, lazyUpdate, prevOption, prevClickEvent]);
  // 监听窗口大小变化,当窗口大小变化时,重新渲染图表
  useWindowResize(() => {
    if (chartInstanceRef.current) {
      chartInstanceRef.current?.resize();
    }
  });
  return <div style={{ ...style }} className={className} ref={chartRef}></div>;
};

export { CommonChart };

3. 使用到的全局hook代码

c 复制代码
// @/utils/index
// 深度判断两个对象某个属性的值是否相等
export const isDeepEqual = (left: any, right: any, ignoredKeys?: string[]): boolean => {
  const equal = (a: any, b: any): boolean => {
    if (a === b) return true

    if (a && b && typeof a === 'object' && typeof b === 'object') {
      if (a.constructor !== b.constructor) return false

      let length
      let i
      if (Array.isArray(a)) {
        length = a.length
        if (length !== b.length) return false
        for (i = length; i-- !== 0;) {
          if (!equal(a[i], b[i])) return false
        }
        return true
      }

      if (a instanceof Map && b instanceof Map) {
        if (a.size !== b.size) return false
        for (i of a.entries()) {
          if (!b.has(i[0])) return false
        }
        for (i of a.entries()) {
          if (!equal(i[1], b.get(i[0]))) return false
        }
        return true
      }

      if (a instanceof Set && b instanceof Set) {
        if (a.size !== b.size) return false
        for (i of a.entries()) if (!b.has(i[0])) return false
        return true
      }

      if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags
      if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf()
      if (a.toString !== Object.prototype.toString) return a.toString() === b.toString()

      const keys = Object.keys(a)
      length = keys.length
      if (length !== Object.keys(b).length) return false

      for (i = length; i-- !== 0;) {
        if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false
      }

      for (i = length; i-- !== 0;) {
        const key = keys[i]

        if (key === '_owner' && a.$$typeof) {
          // React
          continue
        }

        if (ignoredKeys && ignoredKeys.includes(key)) {
          continue
        }

        if (!equal(a[key], b[key])) return false
      }

      return true
    }
    // eslint-disable-next-line no-self-compare
    return a !== a && b !== b
  }
  return equal(left, right)
}
--------------------------------------------------------------------------
// @/hooks/index.ts
/**
 * Returns the value of the argument from the previous render
 * @param {T} value
 * @returns {T | undefined} previous value
 * @see https://react-hooks-library.vercel.app/core/usePrevious
 */
export function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>()

  useEffect(() => {
    ref.current = value
  }, [value])

  return ref.current
}

export function useWindowResize(callback: (event: UIEvent) => void) {
  useEffect(() => {
    window.addEventListener('resize', callback)
    return () => window.removeEventListener('resize', callback)
  }, [callback])
}

4. 使用方式

c 复制代码
// 引入组件和echarts
import { CommonChart } from "@/components/Echarts/common";
import echarts from "echarts/lib/echarts";
// 使用
const useOption = () => {
  return (data: any): echarts.EChartOption => {
    return {
      color: ["#ffffff"],
      title: {
        text: "图表y轴时间",
        textAlign: "left",
        textStyle: {
          color: "#ffffff",
          fontSize: 12,
          fontWeight: "lighter",
          fontFamily: "Lato",
        },
      },
      grid: {
        left: "2%",
        right: "3%",
        top: "15%",
        bottom: "2%",
        containLabel: true,
      },
      xAxis: [
        {
          axisLine: {
            lineStyle: {
              color: "#ffffff",
              width: 1,
            },
          },
          data: data.map((item: any) => item.xTime),
          axisLabel: {
            formatter: (value: string) => value,
          },
          boundaryGap: false,
        },
      ],
      yAxis: [
        {
          position: "left",
          type: "value",
          scale: true,
          axisLine: {
            lineStyle: {
              color: "#ffffff",
              width: 1,
            },
          },
          splitLine: {
            lineStyle: {
              color: "#ffffff",
              width: 0.5,
              opacity: 0.2,
            },
          },
          axisLabel: {
            formatter: (value: string) => new BigNumber(value),
          },
          boundaryGap: ["5%", "2%"],
        },
        {
          position: "right",
          type: "value",
          axisLine: {
            lineStyle: {
              color: "#ffffff",
              width: 1,
            },
          },
        },
      ],
      series: [
        {
          name: t("block.hash_rate"),
          type: "line",
          yAxisIndex: 0,
          lineStyle: {
            color: "#ffffff",
            width: 1,
          },
          symbol: "none",
          data: data.map((item: any) => new BigNumber(item.yValue).toNumber()),
        },
      ],
    };
  };
};
const echartData = [
  { xTime: "2020-01-01", yValue: "1500" },
  { xTime: "2020-01-02", yValue: "5220" },
  { xTime: "2020-01-03", yValue: "4000" },
  { xTime: "2020-01-04", yValue: "3500" },
  { xTime: "2020-01-05", yValue: "7800" },
];
const parseOption = useOption();
<CommonChart
  option={parseOption(echartData, true)}
  notMerge
  lazyUpdate
  style={{
    height: "180px",
  }}
></ChartBlock>

5. 效果展示


总结

下一篇讲【首页响应式搭建以及真实数据渲染】。关注本栏目,将实时更新。

相关推荐
二川bro3 分钟前
TypeScript接口 interface 高级用法完全解析
javascript·typescript
Captaincc25 分钟前
这款堪称编程界的“自动驾驶”利器,集开发、调试、提 PR、联调、部署于一体
前端·ai 编程
我是小七呦35 分钟前
万字血书!TypeScript 完全指南
前端·typescript
simple丶39 分钟前
Webpack 基础配置与懒加载
前端·架构
simple丶43 分钟前
领域模型 模板引擎 dashboard应用列表及配置接口实现
前端·架构
冰夏之夜影44 分钟前
【css酷炫效果】纯css实现液体按钮效果
前端·css·tensorflow
1 小时前
告别手写Codable!Swift宏库ZCMacro让序列化更轻松
前端
摘笑1 小时前
vite 机制
前端
Channing Lewis2 小时前
API 返回的PDF是一串字符,如何转换为PDF文档
前端·python·pdf
海盗强2 小时前
css3有哪些新属性
前端·css·css3