我们直接从一个小小的需求入手
实现这个导出数据的功能
思路(伪代码)
- 获取查询的参数
- 验证参数是否合规
- 把{}格式参数转为string
- 加上项目publicPath
- 执行导出
代码实现
<Button onClick={exportData}>导出数据</Button>
const verification = [
{
verify: i => !!i,
tip: '请输入姓名'
}
];
function exportData() {
// 获取数据(使用的form-render)
const tempParams = form.getValues();
// 验证参数
if (!tempParams.name) return message.error('请输入姓名');
// 用这种方式写会更优雅一点
// for (let i = 0; i < verification.length; i++) {
// if (verification[i].verify()) return message.error(verication[i].tip);
// }
// 参数类型转换
const params = JSON.stringify(tempParams)
.replace(/:/g,"=")
.replace(/"/g, '')
.replace(/,/g, '&')
.match(/\{([^)]*)\}/)?.[1];
// 拼接地址
const goalAddress = `https://123.123:123?${params}`;
// 执行导出
const a_DOM = document.createElement('a');
a_DOM.href = goalAddress;
a_DOM.id = 'a_DOM';
a_DOM.setAttribute('style', 'display: none');
a_DOM.click();
a_DOM.remove();
}
存在问题
- 代码冗余
- 难以理解
根据存在的问题,我们对代码进行,把每个功能单独拿出来
<Button>导出数据</Button>
const verification = [
{
verify: i => !!i,
tip: '请输入姓名'
}
];
function exportData() {
// const tempParams = form.getValues();
// const temp1 = verifyParams(tempParams);
// const temp2 = paramsTransToUrl(temp1);
// const temp3 = goalAddress(temp2);
// const temp4 = exportExcel(temp3);
exportExcel(goalAddress(paramsTransToUrl(verifyParams(form.getValues()))));
}
function verifyParams(params: Record<string, string>) {
for (let i = 0; i < verification.length; i++) {
if (verification[i].verify()) {
message.error(verication[i].tip);
return ({});
}
}
return params;
}
function paramsTransToUrl(params: Record<string, string>) {
return JSON.stringify(tempParams)
.replace(/:/g,"=")
.replace(/"/g, '')
.replace(/,/g, '&')
.match(/\{([^)]*)\}/)?.[1];
}
function goalAddress(params: string) {
return `https://123.123:123?${params}`;
}
function exportExcel(url: string) {
const a_DOM = document.createElement('a');
a_DOM.href = url;
a_DOM.id = 'a_DOM';
a_DOM.setAttribute('style', 'display: none');
a_DOM.click();
a_DOM.remove();
}
存在问题
- 主函数确实简洁了不少,但是代码可读性并没有提高
优化思路
- 可以看到最关键哪一行代码被嵌套了太多层,而且是内部函数的出参是外层函数的入参,根据这个特性,我们需要实现一个功能性函数,这个功能性函数可以把这些函数链接起来。
export const compose = (...funcList: Function[]) => {
if (funcList.length === 0) return (arg: any) => arg;
if (funcList.length === 1) return funcList[0];
return funcList.reduce((a, b) => (...args: Function[]) => b(a(...args)));
}
- 这个compose(也可以叫pipe)函数就是函数式编程里很关键的一个方法,他可以把函数组合起 优化后的代码
function exportData() {
compose(
form.getValues,
verifyParams,
paramsTransToUrl,
goalAddress,
exportExcel
)();
}
存在问题
- 当我把鼠标移到compose上,显示出来的类型是这样的
- 虽然compose的类型确实是Function,但是这个compose无法定义入参的类型,也无法定义出参的类型,在实际项目使用中还是又些不便,我们对compose这个功能函数修改一下
export const compose = <T, K>(...funcList: Function[]) => {
if (funcList.length === 0) {
return (arg: T) => arg as unknown as K
}
if (funcList.length === 1) {
return funcList[0] as (arg: T) => K
}
const func = funcList.reduce((a, b) => (...args: Function[]) => b(a(...args)))
return func as unknown as (arg: T) => K;
}
这样我们就可以自己定义入参出参类型了,最终代码
function exportData() {
const action = compose<void, void>(
form.getValues,
verifyParams,
paramsTransToUrl,
goalAddress,
exportExcel
);
action();
}
从该例子中得出的函数式编程的优点
- 容易修改,容易扩展
当需求改变时,这些高度独立的函数可以互不影响。修改了A并不需要同步修改B
- 容易理解,每个代码文件行数少了
可以吧exportExcel等公共函数拆分到公共until文件里,相信每一个100多行文件看起来应该都挺赏心悦目的吧
- 更符合思维习惯
相比于嵌套式写法的函数,通过compose的函数就像我们写数学式子一样,一个变量从左到右。每一个函数都是一个纯函数,那么这个函数就像一个管道,compose正是把这些一个个的管道连接起来,实现了函数式编程最基本的用法
- 处理数据方式上有天然的优势
咱们前端业务方面做的事儿无非就是:接收数据,处理数据,展示数据。所以说,我们前端很适合使用函数式编程
使用函数式编程的一些注意事项
- 把函数理解为出参对入参的映射
- 函数式编程主打一个“纯”字,它要求函数必须纯,也就是相同的输入一定会得到相同的输出
- 尽量把函数写作只接受一个参数。可是又些时候我们不得不让函数接收两个或以上数量的参数,我们可以借助curry函数转换一下
export const curry = (func: Function) => {
const argumentsLength = func.length;
const curried = (...args: any[]) => {
if (args.length < argumentsLength) {
return (...rest: any[]) => curried(...args, ...rest);
}
return func(...args);
};
return curried;
}
我个人未解决的问题
- 错误处理
就拿当前例子来说,假如verifyParams函数在处理数据的时候,报错了,那么接下来的方法中,必须要对这个返回的错误的数据做处理,这样很麻烦。。肯定有统一的错误处理方法,只是我没有想到而已
- 不过也可以通过直接在写代码的时候注意一下,临时解决,比如我就在发生错误的时候返回了一个空的对象
个人关于函数式编程在实际项目中使用时的一些想法
- 在学习函数式编程的过程中,尤其是在学习完涵子之后,就有一种我有一个金箍棒,但我不是孙悟空的感觉。Maybe涵子,Either涵子,Task涵子之类的实现确实很优雅,很巧妙。我一直致力于如何把这些涵子使用到实际项目中,但是想了好久都没有用武之地。总不能后端返回的数据我再用涵子包裹一下,明明可以直接拿来使用
- 后来发现那些涵之的用武之力不在业务开发流程中,而在项目基础方法中,比如我们在开发公众号小程序之类时,每次登陆,唤起支付,获取opendid之类的操作,我们就可以把这些方法全部写在一个类当中,在写这个类的时候,就可以用到函数式编程概念里的涵子
- 真正在业务流程中使用到的其实还是compose,说实话,curry都用的不多。
- compose使用熟练的时候也就理解了函数式编程中数据流的概念
- 没必要把一个项目全部都使用最纯粹的函数式编程,我以前这种想法有点幼稚,在项目中也不可能实现,总有一些奇怪的需求或使用情景导致相同的输入没有得到相同的输出。
- 尽力而为即可