3

前序

LZ 之前工作一直在用 Vue,但最近听说 Vue 新版也要 All IN JS,所以想着干脆换到 React 算了,所以目前在学习 React + TS + Hook,顺手拿了一个老项目重构,今天主要讲 React 封装 Echarts 公共组件, 因为第一次正式搞,所以本文中如果有 React 代码哪里不规范还请大佬们批评指正哈!

目录:

  1. 需求分析
  2. 技术评估
  3. 实现思路
  4. 测试优化
  5. 总结分享

1. 需求分析

  • ECharts 图表要用到很多,所以要将公共部分抽取成组件减少重复代码
  • ECharts 是需要操作到真实dom的第三方库,和MVVM框架一起使用需要做一些特殊处理
  • Vue项目中使用的Vue-ECharts,组件自动去处理实例挂、 数据更新等
  • React也有echarts-for-react等优秀的开源组件,但为了学习和更舒适的使用,我们需要自己去造新的轮子
  • 参考资料:

2. 技术评估

我们需要用到四个东西:

  • React
  • React Hook
  • Echarts
  • TypeScript

3. 实现思路

1) 一个组件需要的参数配置 ChartProps

参数 描述 必填
key string 用于保持多图表时每一个图表的独立性
option object 或者 null Echarts 配置参数
style { width: string, height: string } 用于保持多图表时每一个图表的独立性
className string 组件样式类 className
onRender onRender?(instance): void; 渲染时回调函数,返回图表示例

2) 参数类型检查接口 interface

interface ChartProps {
    key: string;
    option: object | null;
    style: {
        width: string;
        height: string;
    };
    className?: string;
    onRender?(instance): void;
}

3) 基础组件 Chart.tsx

import * as React from "react";
import echarts from "echarts";

const Chart = (props: ChartProps): React.ReactElement => {
    // 挂载节点
    let chartDom = null;

    // 元素挂载到浏览器事件
    const refOnRender = (el): void => chartDom = el;
    
    // 返回组件
    return React.createElement("div", {
        ref: refOnRender,
        style: props.style,
        className: props.className
    });

};

export default Chart;

4) 当组件挂载到真实DOM,初始化Echarts实例,使用Hook

// 生命钩子函数
type Callback = () => void;
React.useEffect((): Callback => {

    // 加载状态
    function showLoading(instance): void {
        instance.showLoading("default", {
            text: "",
            color: "#c23531",
            textColor: "#000000",
            maskColor: "rgba(255, 255, 255, 0.8)",
            zlevel: 0
        });
    }

    // 获取实例对象
    let instance = echarts.getInstanceByDom(chartDom) || echarts.init(chartDom);

    // 默认加载状态
    showLoading(instance);
    
    // 如果存在参数,渲染图表
    if (props.option) {
        // 关闭加载状态
        if (instance) instance.hideLoading();
        // 渲染图表
        instance.setOption(props.option);
    }
    
}, [props.option]);

5)浏览器窗口大小变化时图表大小自适应重绘

// 大小自适应
const resize = (): void => instance.resize();
window.removeEventListener("resize", resize);
window.addEventListener("resize", resize);

6) 给图表加动画需要图表实例,设置回调函数将图表实例返回

// 回调函数返回实例
if (props.onRender) props.onRender(instance);

7) 组件销毁时清除 监听器组件状态值

// 销毁并清除状态
return (): void => {
    echarts.dispose(instance);
    window.removeEventListener("resize", resize);
};

8) 最终完整组件

import * as React from "react";
import echarts from "echarts";

/**
 * 参数列表
 * key: string; 唯一值
 * option: object | null; 图表数据
 * style: {
 *      width: string; 图表宽度
 *      height: string; 图表高度
 * };
 * className?: string; 图表CSS样式类名称
 * onRender?(instance): void; 图表回调函数返回图表实例
 */

interface ChartProps {
    key: string;
    option: object | null;
    style: {
        width: string;
        height: string;
    };
    className?: string;

    onRender?(instance): void;
}

const Chart = (props: ChartProps): React.ReactElement => {

    // 挂载节点
    let chartDom = null;

    // 生命钩子函数
    type Callback = () => void;
    React.useEffect((): Callback => {
        console.log("useEffect");

        // 加载状态
        function showLoading(instance): void {
            instance.showLoading("default", {
                text: "",
                color: "#c23531",
                textColor: "#000000",
                maskColor: "rgba(255, 255, 255, 0.8)",
                zlevel: 0
            });
        }

        // 获取实例对象
        let instance = echarts.getInstanceByDom(chartDom) || echarts.init(chartDom);

        // 大小自适应
        const resize = (): void => instance.resize();
        window.removeEventListener("resize", resize);
        window.addEventListener("resize", resize);

        // 默认加载状态
        showLoading(instance);

        // 渲染图表
        if (props.option) {
            if (instance) instance.hideLoading();
            instance.setOption(props.option);
        }

        // 回调函数返回实例
        if (props.onRender) props.onRender(instance);

        // 销毁并清除状态
        return (): void => {
            echarts.dispose(instance);
            window.removeEventListener("resize", resize);
        };

    }, [chartDom, props]);


    // 元素挂载到浏览器事件
    const refOnRender = (el): void => chartDom = el;
    
    // 返回组件
    return React.createElement("div", {
        ref: refOnRender,
        style: props.style,
        className: props.className
    });

};

// 导出组件模块
export default Chart;

测试优化

你可以:

  1. 自行搭建React+TS+Echarts开发环境
  2. 使用 Clone 本项目测试(暂不支持 sry)

主要代码 Test.tsx

import * as React from "react";
// 导入 Chart 组件
import Chart from "../chart";

const chartEmpty = {
    title: {
        text: "暂无数据",
        show: true,
        textStyle: {
            color: "grey",
            fontSize: 20
        },
        left: "center",
        top: "center"
    }
};

const Test= (): React.ReactElement => {
    // 图表1数据
    let [chart1Data, setChart1Data] = React.useState(null);
    // 图表2数据
    let [chart2Data, setChart1Data] = React.useState(null);
    
    // 防护监控数据 实例
    let [chart1, chart2] = [null, null];
    
    // 模拟异步更新图表数据
    function updateChart(): void{
        let opts: null;
        
        chartEmpty.title.text = "图表 1 暂无数据" + (+new Date());
        opts = JSON.parse(JSON.stringify(chartEmpty));
        setChart1Data(opts);
        
        chartEmpty.title.text = "图表 2 暂无数据" + (+new Date());
        opts = JSON.parse(JSON.stringify(chartEmpty));
        setChart2Data(opts);
    }
    
    // 获取图表实例,添加自定义图表事件
    React.useEffect((): void => {
        console.log("chart1", chart1);
    }, [chart1]);
    
    // 返回组件
    return (
        <div>
           <Chart
                key="chart1"
                className="chart1"
                option={chart1Data}
                onRender={(e): void => chart1 = e}
                style={{width: "100%", height: "400px"}}/>
           <hr>       
           <Chart
                key="chart2"
                className="chart2"
                option={chart2Data}
                style={{width: "100%", height: "400px"}}/>
           <button onClick={updateChart}> 异步更新图表数据 </button>
        </div>
    )
}

export default Test;

总结分享

学习了React组件的封装,由此及彼,以后其他第三方库应该也可以轻松集成了
学习了React Hook使用方法。
学习了React + TS的开发模式。
不到100行代码的组件相比于echarts-for-react等其他优秀组件的少了很多高级和细节自定义。(开源的话会加进去吧)
欢迎大佬检阅。


烈虎
68 声望2 粉丝

失去人性,失去很多; 失去兽性,失去一切。