前言
在项目开始初期,往往都是有组里的“大佬”为我们搭建或者说改造一个项目使用的前端项目框架,通常都会包含一些常用的库和函数:比如 router 路由、store 数据共享、登录和异常页、动态路由和按钮权限处理等等。但是在项目进行过程中,都需要大家一起参与!不管是小白也好、大佬也好,在处理一些基础的表单表格的时候,基本思路和代码逻辑应该都是差不多的。
所以为了统一和复用这部分功能的代码,来减少代码量、降低“小白”们编写代码时会花费的时间,我们都会创建一个 Utils 工具函数目录,用来存放我们抽离出来的所有工具函数。
那么如何抽离和封装一个工具函数呢?我们需要从以下这些步骤开始。
1. 初始的逻辑抽离
这一部分一般都是发生在我们刚刚意识到我们现在编写的这一段代码在以后我可能还会用到的时候,我们会想到的处理方式。
以我项目中的一个例子来说:有一个很大的字典对象,存放了所有前后端定义的字段数组;但是在详情页显示的时候一般都需要通过 value 字段来查找对应的 label。
此时一般都是建立一个函数专门用来查找,或者将字段数组转为 map 格式(目前一般都是用对象)。我们当时选择了第二种方案:
export const Enums = { protocolType: [ { label: "label 1", value: 1 }, { label: "label 2", value: 2 }, { label: "label 3", value: 3 } ], nodeType: [ { label: "label 1", value: 1 }, { label: "label 2", value: 2 } ] } export function arr2map (arr) { return arr.reduce((mmap, item) => { mmap[item.value] = item.label; return mmap; }, {}); } export const EnumsMap = Object.keys(Enum).reduce((eEmap, groupName) => { eEmap[groupName] = arr2map(Enum[groupName]); return eEmap; }, {});
这里我们编写了一个函数 arr2map ,用来将我们字典的对象数组转化为一个 value: label
格式的对象。
此时在当前情况下这段逻辑是没有一点问题的。
2. 遇到问题
但是在后来,我们遇到了一个新问题:arr2map 只支持 [{ label: xxx, value: xxx }]
的格式,不能用于其他场景。
所以,我们又对其进行了一次改造(当然,改造必须影响最小化,即不会影响以前的代码,也不需要重新对以前的代码进行修改)
export function notNull(val) { return val !== undefined && val !== null; } export function arr2map(arr = [], props = { label: "label", value: "value" }) { return arr.reduce((mmap, item) => { mmap[item[props.value]] = notNull(props.label) ? item[props.label] : true; return mmap; }, {}); }
这时我们的数组转对象的函数就算是比较成熟了。可以支持一个配置项来控制我们从对象数组中提取对象的哪些元素来生成新的对象。
3. 新的调整
上面的 arr2map 函数,我相信也已经适应了大部分的使用场景。但是,我们又遇到了一个更难受的问题:不只是需要转成 {value: label}
的情况,还需要得到对应字段的所有属性。
比如 [{label: 'label1', value: 1, disabled: true, author: 'xxx'}]
需要转成 { 1: {label: 'label1', value: 1, disabled: true, author: 'xxx'}}
所以此时有需要对 arr2map 进行新的改造。
export function notNull(val) { return val !== undefined && val !== null; } export function arr2map(arr = [], props = { label: "label", value: "value" }, retain = false) { return arr.reduce((mmap, item) => { if (retain) { mmap[item[props.value]] = item } else { mmap[item[props.value]] = notNull(props.label) ? item[props.label] : true; } return mmap; }, {}); }
4. 其他挑战
上面的一部分代码我相信大部分的同学都能正确的处理,或者想到更优秀完善的代码。
但是在后续的项目推进中,又接到了产品的一系列新的需求。其中一个就是:根据一个值从树型数组中拿到所有上级节点的 label 值组成一个完整的字符串路径返回。
这个功能一般在 组织机构树、关系树 等场景中。为了做到后续能兼容更多的场景更多的数据类型,所以然需要接收一些配置项。最终的代码如下:
/** * 获取一个数据在树形数组中对应的名称 ( 场景:根据code在组织树中查询对应的组织名称 ) * @param { array } tree 包含子节点的数据对象 * @param { * } value 当前查询的值, 一般是字符串或者数字 * @param {{key?: string, label?: string, children?: string}} props 默认关键字(key: 查询值键名,label: 名称键名) * @param { ?object } options 配置项 * @return { string | undefined } 名称 * */ export function getTreeNodeLabel(tree, value, props = {}, options = {}) { let { key = "code", label = "label", children = "children" } = props; let { splice = true, hideFirst = false } = options; for (let node of tree) { if (node[key] === value) { return node[label]; } if (notEmpty(node[children])) { let res = getTreeNodeLabel(node[children], value, props, { splice }); if (res) { if (hideFirst) { return res; } return splice ? `${node[label]}/${res}` : res; } } } return undefined; }
这里接收两个配置对象:props 和 options。
- props: 用来配置数据获取以及确认递归对象,保证可以通过配置这几个参数来适应多种属性数据格式
- options:用来配置输出数据格式;splice 确认是否需要分割线,hideFirst 是否需要隐藏顶级节点
后面其实还可以扩展,如果将隐藏顶级节点 label 改为 隐藏几级节点的label,分割线也可以配置,或者支持用函数来处理等等。
5. 总结
从上面两个场景来看,工具函数主要是 “对业务数据进行处理,得到另外一种预期中的数据格式”,那么 “处理同种业务场景,或者需要统一显示样式等情况,一般都是用封装业务组件来完成。
数据处理(转换),个人感觉最常见的就是对数组或者对象的操作,不管是合并、扁平化等等,在编写这类工具函数的过程中,不仅可以增加我们对编写工具函数时注意事项的理解、也能增加我们对算法、函数式编程的掌握。