React 函数式编程实战

480 阅读5分钟

React 函数式编程实践

按时发生的发生1.jpg

光说不练都是假把戏,概念说多了都是废话,与其从官网copy很多概念过来,不如直接上代码!现在看看函数式编程思想与React的搭档

组件渲染效果

组件组合.png

组件完整代码

// 采用的技术栈: React Hooks、Echarts、AntD-4.x、 RamdaJS、Typescript
import React, { useState, useEffect } from "react";
import { Select, Empty, Row, Col } from 'antd';
const { Option } = Select;
import { compose, mergeDeepRight, ifElse } from 'ramda';
import "./report.less"
// 柱状图组件
import BarChart from '@/components/Charts/BarChart';
// 通用组件
import UniversalvChart from '@/components/Charts/UniversalvChart';

interface DataItem {
    title:string; 
    options?: String[],
    chartOptions: {},
    containerId?: string;
    onChange?: (value:string)=>void
}


// 填充图表
const fillChart = (Component:any, {chartOptions, containerId}:DataItem, RenderChart:any)=>{
    return (
        <>
            <Component />
            <div className="echart-container-body">
                {
                    <RenderChart id={containerId} options={chartOptions} />
                }
            </div>
        </>
    );
}

// 填充Header
const fillHeaderContainer = (Component:any):Function=>{
    return ()=><>
        <div className="echart-container-header">
            <Component />
        </div>
    </>
}


// 组装头部标题
const fittTitle = (data:DataItem)=>{

    return ()=> (
        <div className="header-title">
            <h3>{data.title}</h3>
        </div>
    )
}


// 组装下拉菜单选择器
const fittSelectPicker = (Component:any, {options, onChange}:DataItem)=>{
    return ()=> <>
        <Component />
        <div className="header-filter">
            <Select defaultValue="请选择" style={{ width: 120 }} onChange={onChange}>
                {
                    options.map(item => <Option key={'option_'+item} value={item}>{item}</Option>)
                }
            </Select>
        </div>
    </>
}


// 通过组合生成柱状图组件
const RenderBarEchart = (props:DataItem)=>{

    return compose(
        (Component)=>{
            // 判断展示空组件还是展示图表组件
			// 通过Ramda 提供的ifElse函数进行条件判断,如果有数据显示柱状图,没有数据则采用空组件样式
            let RenderComponent = ifElse(()=>Object.keys(props.chartOptions).length > 0, ()=>BarChart, ()=>Empty);
            return fillChart(Component, props, RenderComponent());
        }, 
        fillHeaderContainer, 
        (Component)=>{
            return fittSelectPicker(Component, props)
        }, 
        fittTitle
    )(props);
}


// 通过组合生成饼图组件
const RenderPieEchart = (props:DataItem)=>{
    return compose((Component)=>fillChart(Component, props, UniversalvChart), fillHeaderContainer, fittTitle)(props);
}

// 饼图数据
const data2 = {
    title: '渲染饼图', 
    chartOptions: {
        title: {
            text: '',
            subtext: 'Fake Data',
            left: 'center'
        },
        tooltip: {
            trigger: 'item'
        },
        legend: {
            orient: 'vertical',
            left: 'left'
        },
        series: [
            {
                name: 'Access From',
                type: 'pie',
                radius: '50%',
                data: [
                    { value: 1048, name: 'Search Engine' },
                    { value: 735, name: 'Direct' },
                    { value: 580, name: 'Email' },
                    { value: 484, name: 'Union Ads' },
                    { value: 300, name: 'Video Ads' }
                ],
                emphasis: {
                    itemStyle: {
                        shadowBlur: 10,
                        shadowOffsetX: 0,
                        shadowColor: 'rgba(0, 0, 0, 0.5)'
                    }
                }
            }
        ]
    }
};


// 页面组件
const Home = (props:any)=>{

    let [data, setData] = useState({title: '', options:[], chartOptions: {}});

    // 动态设置数据
    useEffect(()=>{
        let timer = setTimeout(()=>{
			// 动态设置柱状图数据
            setData({
                title: "渲染柱状图",
                options: ["lucy", "jack", "Yiminghe"],
                chartOptions: {
                    title: {
                      text: ''
                    },
                    tooltip: {},
                    xAxis: {
                      data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
                    },
                    yAxis: {},
                    series: [
                      {
                        name: '销量',
                        type: 'bar',
                        data: [5, 20, 36, 10, 10, 20]
                      }
                    ]
                }
            });

        }, 2000);

        return ()=>{
            if(timer){
                clearTimeout(timer);
            }
        }
    }, []);


	// 下拉菜单选择,切换柱状图数据
    const onChange = (value:string) => {
        if(value === 'Yiminghe'){
            setData(mergeDeepRight(data, {
                chartOptions: {
                    xAxis: {
                        data: ["Java", "PHP", "C++", "Ruby", "C", "Python"]
                    }
                }
            }))
        }
    };

    return (
        <div>
            <h1>首页</h1>
            <div className="echart-list">
                <Row>
                    <Col span={6}>
                        <div className="echart-container">
                            <RenderBarEchart {...data} containerId="one" onChange={ onChange } />
                        </div>
                    </Col>
                    <Col span={6}>
                        <div className="echart-container">
                            <RenderPieEchart {...data2} containerId="two" />
                        </div>
                    </Col>
                    <Col span={6}></Col>
                    <Col span={6}></Col>
                </Row>
            </div>
        </div>
    );
};

export default Home;

目标

通过组合小组件函数生成需要的(图表)组件函数

组件为空时展示.png

zuzhuang组装.png

分解说明

1.构建标题组件函数

// fittTitle接收上data数据,里面包含头部标题信息, 返回头部标题组件函数
const fittTitle = (data:DataItem)=>{
    return ()=> (
        <div className="header-title">
            <h3>{data.title}</h3>
        </div>
    )
}

2.构建下拉菜单选择器

// fittSelectPicker接收上一个函数返回的组件和需要渲染的options列表数据,返回下拉菜单组件
// onChange事件的回调函数位于同一个函数组件中
const fittSelectPicker = (Component:any, {options, onChange}:DataItem)=>{
    return ()=> <>
        <Component />
        <div className="header-filter">
            <Select defaultValue="请选择" style={{ width: 120 }} onChange={onChange}>
                {
                    options.map(item => <Option key={'option_'+item} value={item}>{item}</Option>)
                }
            </Select>
        </div>
    </>
}

3.构建Header容器组件

// 该组件用于包裹标题组件和下拉菜单组件
// 接收上一个函数返回的组件(标题组件和下拉组件的结合组件,或二选一),返回头部组件
const fillHeaderContainer = (Component:any):Function=>{
    return ()=><>
        <div className="echart-container-header">
            <Component />
        </div>
    </>
}

4.构建图表面板容器组件

// 接收(头部)组件,和容器体需要渲染的组件(可能是图表组件、也可能是空组件)
const fillChart = (Component:any, {chartOptions, containerId}:DataItem, RenderChart:any)=>{
    return (
        <>
            <Component /> {/* 面板头部组件 */}
            <div className="echart-container-body">
                {
                    <RenderChart id={containerId} options={chartOptions} />
                }
            </div>
        </>
    );
}

5.图表(柱状图)组件

import React, { useState, useEffect } from 'react';
import * as echarts from 'echarts';

const BarChart = ({
    id = "chartContainer",
    width = "100%",
    height = "360px",
    options = {}
})=>{

    let [echart, setEchart] = useState(null);

    // 初始化图表
    useEffect(()=>{
        if(echart === null){
            echart = echarts.init(document.getElementById(id))
            setEchart(echart);
        }

        echart.setOption(options);
    }, [options]);

    return (
        <div className="charts-wrapper" id={id} style={{ width: width, height: height }}>

        </div>
    )
};

export default BarChart;

6.通用图表组件(这里用作饼图)

import React, { useState, useEffect } from 'react';
import * as echarts from 'echarts';

const UniversalvChart = ({
    id = "chartContainer2",
    width = "100%",
    height = "360px",
    options = {}
})=>{

    let [echart, setEchart] = useState(null);

    // 初始化图表
    useEffect(()=>{
        if(echart === null){
            echart = echarts.init(document.getElementById(id))
            setEchart(echart);
        }

        echart.setOption(options);
    }, [options]);

    return (
        <div className="charts-wrapper" id={id} style={{ width: width, height: height }}></div>
    )
};

export default UniversalvChart;

7.通过组合生成柱状图组件

组合就应该让组件像水一样流淌在管道中;又或是工厂的流水线;

e8bb9cff55ff45db820b6beae8c0bd7e.jpeg

// 渲染柱状图组件
const RenderBarEchart = (props:DataItem)=>{
    // compose为RamdaJS库提供的compose函数;
    // 如果习惯于从左向右,也可以将函数顺序倒置,采用pipe方法
    return compose(
        // 组装图表头部和主体,返回最终的组件
        (Component)=>{
            // 判断展示空组件还是展示图表组件
	    // 通过Ramda 提供的ifElse函数进行条件判断,如果有数据显示柱状图,没有数据则采用空组件样式
            let RenderComponent = ifElse(()=>Object.keys(props.chartOptions).length > 0, ()=>BarChart, ()=>Empty);
            return fillChart(Component, props, RenderComponent());
        }, 
        // 接收标题组件 | 标题 + 菜单组件
        fillHeaderContainer, 
        // 接收fittTitle返回的组件;因为fittSelectPicker需要接收两个参数,而组合中的函数只能接收一个参数
        (Component)=>{
            // 因为这里需要用到2个参数,所以作为参数返回
            // props位于最顶层作用域,所以使用非常方便
            return fittSelectPicker(Component, props)
        }, 
        // fittTitle为粒度最小的组件,第一个接收参数
        fittTitle
    )(props);  // props为用于渲染的数据
}
  1. 通过组合生成饼图组件
const RenderPieEchart = (props:DataItem)=>{
    // 饼图没有组装下拉菜单; 解析过程同上面柱状图组件
    return compose(
        (Component)=>fillChart(Component, props, UniversalvChart), 
        fillHeaderContainer, 
        fittTitle
   )(props);
}

9.钩子函数中设置数据并渲染

// 页面组件
const Home = (props:any)=>{

    let [data, setData] = useState({title: '', options:[], chartOptions: {}});

    // 动态设置数据
    useEffect(()=>{
        let timer = setTimeout(()=>{
            // 动态设置柱状图数据
            setData({
                title: "渲染柱状图",
                options: ["lucy", "jack", "Yiminghe"],
                chartOptions: {
                    title: {
                      text: ''
                    },
                    tooltip: {},
                    xAxis: {
                      data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
                    },
                    yAxis: {},
                    series: [
                      {
                        name: '销量',
                        type: 'bar',
                        data: [5, 20, 36, 10, 10, 20]
                      }
                    ]
                }
            });

        }, 2000);

        return ()=>{
            if(timer){
                clearTimeout(timer);
            }
        }
    }, []);


    // 下拉菜单选择,切换柱状图数据
    const onChange = (value:string) => {
        if(value === 'Yiminghe'){
            setData(mergeDeepRight(data, {
                chartOptions: {
                    xAxis: {
                        data: ["Java", "PHP", "C++", "Ruby", "C", "Python"]
                    }
                }
            }))
        }
    };

    return (
        <div>
            <h1>首页</h1>
            <div className="echart-list">
                <Row>
                    <Col span={6}>
                        <div className="echart-container">
                            {/* 传递data给柱状图组件 */}
                            <RenderBarEchart {...data} containerId="one" onChange={ onChange } />
                        </div>
                    </Col>
                    <Col span={6}>
                        <div className="echart-container">
                            {/* 传递data2给饼图组件 */}
                            <RenderPieEchart {...data2} containerId="two" />
                        </div>
                    </Col>
                    <Col span={6}></Col>
                    <Col span={6}></Col>
                </Row>
            </div>
        </div>
    );
};

export default Home;

结语

  • 天下没有最厉害的武功,只有更厉害的人!
  • 天下没有最优秀的技术,只有最适合的场景!
  • 函数式编程已经为我们开启了代码编写方式的另一扇窗!
  • 切记: "不能因为你手里有一把锤子,就看什么都是钉子!"