React 函数式编程实践
光说不练都是假把戏,概念说多了都是废话,与其从官网copy很多概念过来,不如直接上代码!现在看看函数式编程思想与React的搭档
组件渲染效果
组件完整代码
// 采用的技术栈: 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;
目标
通过组合小组件函数生成需要的(图表)组件函数
分解说明
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.通过组合生成柱状图组件
组合就应该让组件像水一样流淌在管道中;又或是工厂的流水线;
// 渲染柱状图组件
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为用于渲染的数据
}
- 通过组合生成饼图组件
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;
结语
- 天下没有最厉害的武功,只有更厉害的人!
- 天下没有最优秀的技术,只有最适合的场景!
- 函数式编程已经为我们开启了代码编写方式的另一扇窗!
- 切记: "不能因为你手里有一把锤子,就看什么都是钉子!"