业务中条件渲染的另一种玩法
在开始之前我先介绍一个Antd的扩展组件库 ProTable,
将所有的数据源维护在一个JSON数据中,用函数去处理筛选的逻辑,从而得到我们在当前页所需要的数据结构。Antd中的有一个仓库ProTable,ProTable是一个集合了查询、表格和分页的多功能复合型组件,它将Search部分以及Table部分的数据都维护在一份JSON数据中,用不同的参数去驱动UI,其基本配置如下:
从ProTable的配置表上可以展现出来, ProTable几乎满足一般中后台的表格操作和属性配置的, 从这一种配置型的Schema可以简单分析一下, 将后台管理中的查询表单以及表格属性栏全部集中到一个JSON对象中去管理. 通过两个自定义的字段 hideInSearch、hideInForm 和 hideInTable 来区分该配置项目的所在的模块渲染. 如下: 下列Column定义地就是以上三个模块全部不渲染, 类似于权限控制:
[
{
title: 'A',
dataIndex: 'A',
hideInSearch: true,
hideInForm: true,
hideInTable: true
}
]
现在可以使用数组Api: Array.prototype.filter
, 就可以轻易的完成以上的筛选操作, 让我们来实现一个组合筛选功能函数.
现在, 我越来越认识到, 定义的每一个函数都有着这个函数独有的功能, 不要将多个任务放到一个函数里, 如果需要处理的数据上的问题, 可以将函数留一个入口, 通过一个回调函数去处理我们需要的数据结构, 就如同JS的数组Api一样.
例如, 在JS中的数组Filter
, Array.prototype.filter(cb(element, index, arr)), 就可以根据我们在回调函数中定义的条件, 快速的将数据做一次筛选.
实现一个compose函数
组合函数可以仿写一下Redux的函数, 函数很简单不超过十行, 但是其实现的功能非常强大, 我们现在的多条件组合查询筛选都用它.
function compose (...fn) {
if (fn.length === 0) {
return args => args
}
if (fn.length === 1) {
return fn[0]
}
return fn.reduce((f, g) => (...args) => f(g(args)))
}
运用compose
函数的方法及其简单, 现在定义以下情景, 按步骤实现:
先将一个Emnu类型的对象, 转换为数组;
将转换的数组, 做一次权限控制;
将权限控制的数组, 根据不同字段, 动态添加一些不同的属性;
...
以上情景根据你的业务逻辑, 无限扩展
在日常业务中, 我们极有可能遇到一些及其复杂的逻辑, 通常我们将它们全部在一个函数内部全部实现, 随着业务的升级以及业务逻辑的升级, 原来控制逻辑的代码越来越多, 越来越不好控制. 由此, 我们可以得出一个结论: 必须将复杂的业务逻辑, 一点一点拆分出去, 这样我们就可以实现, 业务环节状态的流转越多, 代码越好控制.
在Redux中, 有一个及其有意思的combineReduce函数, 可以将我们定义的多个Reducer集中为一个Reducer. 所以, 我们可以仿写Reducer的流程控制, 来实现复杂的业务逻辑.
// 将一个Emnu转换为 Array
// @params {obj} Object
// {
// 1: { label: '便签1', value: '0' },
// 2: { label: '便签2', value: '2' }
// }
function emnuConvertToArray (obj = {}) {
try {
if (obj === null && obj === undefined && obj !== 'object') return obj
return Object.entries(obj).map(([key, value]) => ({ [value.label]: key, ...value }))
} catch (error) {
Error(error)
}
}
const ops = {
getProto: item => item.value === 2 ? ({ ...item, name: 'yes' }) : ({ ...item, name: 'no' })
}
function filterColumnsBy (arr = [], cb) {
if (!Array.isArray(arr)) return arr
return [arr.filter(item => cb ? cb(item) : (item.value > 1)), ops]
}
// 如果value 等于2, 添加一些字段
function appendProtoBy (args) {
const [arr, ops] = args
if (!Array.isArray(arr)) return arr
return arr.map(item => ({ ...item, extend: ops.getProto(item) }))
}
// 组合三个条件分支, 返回需要的数据结构
var composeColumns = compose(appendProtoBy, filterColumnsBy, emnuConvertToArray)
var obj = {
1: { label: 'aaa', value: 1 },
2: { label: 'bbb', value: 2 },
3: { label: 'ccc', value: 3 }
}
var columns = composeColumns(obj)
console.log('current is columns: ', columns)
如上, 如果任意环节需要改变逻辑, 就可以直接找到对应的逻辑函数, 改掉对应的逻辑就可以啦.
实现一个简单的筛选挑选功能
依次将筛选hideInTable, hideInSearch 的函数定义, 然后组合外部传入的函数, 将自定义筛选的函数留一个入口, 如下:
function filterSearch (origin = [], cb) {
return origin.filter((item, index, arr) => cb ? cb(item, index, arr) : !item.hideInTable)
}
filter
的筛选功能极其简单快捷, 也是我们常用的过滤功能的手段. 但是, 在目前看来很多的前端还没有积极的使用ES6中的api, 还在固执的, 守旧的使用着落后的手段去处理极其简单的筛选+挑选功能.
比如, 同一个管理后台的订单列表页, 不同的身份有着不同的查询条件, 或者是不同的渲染表格, 我看过一段代码, 差点把我看吐了. 是这么处理的:
// 定义公共部分的 查询条件
const commonQuery = [
{ label: 'AAA', field: 'AAA' },
{ label: 'BBB', field: 'BBB' }
]
// 定义客服身份的查询
const serviceQuery = [
{ label: 'service', field: 'service' }
]
// 定义商务身份的查询
const businessQuery = [
{ label: 'business', field: 'business' }
]
const composeQuery = []
if (type === 'service') {
composeQuery = commonQuery.concat(serviceQuery)
} else if (type === 'business') {
composeQuery = commonQuery.concat(businessQuery)
}
以上代码的优化手段有很多, 每一个人都有不一样的解法, 下面列出我的解决方法:
const agreevQuery = [
{ label: 'AAA', field: 'AAA', auth: ['service'] },
{ label: 'BBB', field: 'BBB', auth: ['business'] },
{ label: 'CCC', field: 'CCC', auth: ['type'] }
]
const AUTH_TYPE = 'business'
const filterColumns = (origin = []) => origin.filter(item => item.auth.indexOf(AUTH_TYPE) > -1)
当然如果对页面的表单项的排序有着强烈的需求, 我们还可以定义一个字段, 去定义数组项目的Index索引, 其配置如下:
const agreevQuery = [
{ label: 'AAA', field: 'AAA', auth: ['service'], index: 1 },
{ label: 'BBB', field: 'BBB', auth: ['business'], index: 3 },
{ label: 'CCC', field: 'CCC', auth: ['type'], index: 2 }
]
const AUTH_TYPE = 'business'
// 控制排序
const sortColumns = (origin = []) => origin.sort((a, b) => a - b)
// 处理Index序号
const filterIndexColumns = (origin = []) => origin.map(item => ({ ...item, index: typeof item.index === 'function' ? item.index(item) : item.index }))
// 权限过滤
const filterColumns = (origin = []) => origin.filter(item => item.auth.indexOf(AUTH_TYPE) > -1)
const composeColumns = compose(sortColumns, filterIndexColumns, filterColumns)
const columns = composeColumns(agreevQuery)
console.log('current is columns: ', columns)
当我们慢慢地尝试将复杂的, 复合式的逻辑一条一条的抽离, 然后将这一些逻辑函数, 一个一个的组合, 那我们得出的结果就是我们需要的数据. 这样去维护逻辑和接口, 数据的流转就变得清晰透明起来, 我们可以很快速的排查到错误位置.
我们现在只是实现了表格的多条件渲染以及查询表单的多条件渲染, 在业务中还存在各种条件渲染, 以及各种各样的需求去实现, 我们不可能定义实现一个百分百匹配的模型, 所以我们设计模型的时候, 需要将一些变动的模块或者的方法, 留出一个入口, 允许外部传入函数, 组合模型内部的方法去实现需求.