React+TS前台项目实战(二十六)-- 高性能可配置Echarts图表组件封装
文章目录
- 前言
- CommonChart组件
- 1. 功能分析
- 2. 代码+详细注释
- 3. 使用到的全局hook代码
- 4. 使用方式
- 5. 效果展示
- 总结
前言
Echarts图表在项目中经常用到,然而,重复编写初始化,更新,以及清除实例等动作对于开发人员来说是一种浪费时间和精力。因此,在这篇文章中,将封装一个 “高性能可配置Echarts组件” ,简化开发的工作流程,提高数据可视化的效率和质量。
CommonChart组件
1. 功能分析
(1)可以渲染多种类型的图表,包括折线图、柱状图、饼图、地图和散点图
(2)通过传入的 option 属性,配置图表的各种参数和样式
(4)通过传入的 onClick 属性,处理图表元素的点击事件
(5)通过传入的 notMerge 属性,控制是否合并图表配置
(6)通过传入的 lazyUpdate 属性,控制是否懒渲染图表
(7)通过传入的 style 属性,设置图表容器的样式
(8)通过传入的 className 属性,自定义图表容器的额外类名
(9)通过监听窗口大小变化,自动调整图表的大小
(10)使用 usePrevious、useWindowResize 和 useEffect 等钩子来提高组件性能并避免不必要的渲染
2. 代码+详细注释
// @/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(null); const chartInstanceRef = useRef(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 { ...style }} className={className} ref={chartRef}; }; export { CommonChart };
3. 使用到的全局hook代码
// @/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(value: T): T | undefined { const ref = useRef() 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. 使用方式
// 引入组件和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(); parseOption(echartData, true)} notMerge lazyUpdate style={{ height: "180px", }}
5. 效果展示
总结
下一篇讲【首页响应式搭建以及真实数据渲染】。关注本栏目,将实时更新。
文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。