Skip to content

业务中条件渲染的另一种玩法:从 ProTable 到组合函数

摘要: 将查询条件和表格配置集中到一个 JSON 数据中,用函数组合处理筛选逻辑,比手写 if/else 更清晰可维护。


背景:ProTable 的配置思路

ProTable 是 Ant Design 的扩展组件库,集合了查询、表格和分页功能。它将 Search 和 Table 的数据维护在一份 JSON 中,用不同参数驱动 UI。

基本配置:

image-1

从 ProTable 的配置可以看出,查询表单和表格属性栏全部集中到一个 JSON 对象中管理。通过 hideInSearchhideInFormhideInTable 三个字段来区分该配置项所在的模块渲染。例如,以下 Column 定义表示三个模块全部不渲染(类似权限控制):

javascript
[
  {
    title: 'A',
    dataIndex: 'A',
    hideInSearch: true,
    hideInForm: true,
    hideInTable: true
  }
]

Array.prototype.filter 就可以轻易完成筛选操作。


组合函数 compose

组合函数可以仿写 Redux 的实现,不超过十行,但功能非常强大:

javascript
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 的方法很简单。例如:

  1. 将 Enum 类型对象转换为数组
  2. 做一次权限控制筛选
  3. 根据不同字段动态添加属性
  4. 根据业务逻辑无限扩展

日常业务中经常遇到复杂逻辑全部写在一个函数内部的情况,随着业务升级,控制逻辑的代码越来越多。结论:必须将复杂业务逻辑一点一点拆分出去,业务环节状态流转越多,代码越好控制。


示例:Enum 转数组 + 权限 + 扩展属性

javascript
// 将 Enum 转换为 Array
// @params {obj} Object
// {
//   1: { label: '标签1', value: '0' },
//   2: { label: '标签2', value: '2' }
// }
function enumConvertToArray (obj = {}) {
  try {
    if (obj === null || obj === undefined || typeof obj !== 'object') return obj
    return Object.entries(obj).map(([key, value]) => ({ [value.label]: key, ...value }))
  } catch (error) {
    console.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]
}

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, enumConvertToArray)

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)

如果任意环节需要改变逻辑,直接找到对应的逻辑函数修改即可。


对比:传统方式 vs 组合方式

看过一段管理后台的代码,差点看吐了:

javascript
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)
}

优化后:

javascript
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 字段:

javascript
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 sortColumns = (origin = []) => origin.sort((a, b) => a.index - 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)

总结

将复杂的复合式逻辑一条一条抽离,然后一个一个组合,数据流转就变得清晰透明,可以快速排查错误位置。设计模型时,将变动的模块或方法留出入口,允许外部传入函数组合内部方法去实现需求。

MIT License.