table配置化,减少重复代码

698 阅读3分钟

如果我们经常使用element-ui搭建web后台系统页面,就会经常使用如下结构:

<el-table :data="tableData" style="width: 100%">
  <el-table-column label="日期">
    <template slot-scope="scope">
      ...
    </template>
  </el-table-column>
  <el-table-column>
    <template slot-scope="scope">
      ...
    </template>
  </el-table-column>
  <el-table-column>
    <template slot-scope="scope">
      ...
    </template>
  </el-table-column>
  ...
</el-table>

简单的配置

可以看到,如果表格列比较多,需要写重复的el-table-column和template,那么我们首先想到的就是能否将这些el-table-column抽象成一个个配置,通过配置将el-table渲染出来呢?🤔

export default {
  props: {
    columnConfig: { type: Array, default: () => ([]) },
    tableEvents: { type: Object, default: () => ({}) },
    tableProps: { type: Object, default: () => ({}) }
  }
  render (h) {
    const columns = this.columnConfig.map((col) => {
      const { ...props } = col
      const options = { props }
      retutn this.$createElement('el-table-column', options)
    })
    const table = this.$createElement('el-table', {
      ref: 'table',
      props: this.tableProps,
      on: {
        ...tableEvents
      }
    }, columns)
    return table
  }
}

这样,我们可以通过传入像这样的配置就能渲染出table了:

const columnConfig = [  { prop: 'id', label: 'ID' },  { prop: 'name', lable: '姓名' }]
const tableProps = {
  data: [{ id: '1',name: '王小虎' }, { id: '2',name: '王小虎' }],
  border: true
}

1.png

支持传入配置scopedSlots

而template里的slot-scope其实就是h中的scopedSlots,因此只需要把scopedSlots传入到options就可以了,并把this.$createElement传入到配置项scopedSlots中渲染出自定义的dom节点:

...
  const { scopedSlots, ...props } = col
    const options = {
      props: props,
      scopedSlots: scopedSlots ? scopedSlots(this.$createElement) : null
  }
  retutn this.$createElement('el-table-column', options)

  const columnConfig = [
      { type: 'selection', width: 60 }
      { prop: 'id', label: 'ID' },
      { prop: 'name', lable: '姓名', scopedSlots (h) {
        return {
          header ({ column }) {
            return h('span', null, `标题--${column.label}`)
          }
          default ({ row }) {
            return h('el-link', { 
              props: { underline: false }
            }, `我是${row.name}`)
          }
        }
      }
    }
  ]

2.png

对于处理事件,直接从table组件传入,在配置文件接收:

const columnConfig = (handle) => ([
  { type: 'selection', width: 60 }
  { prop: 'id', label: 'ID' },
  { prop: 'name', lable: '姓名', scopedSlots (h) {
    return {
      header ({ column }) {
        return h('span', null, `标题--${column.label}`)
      }
      default ({ row }) {
        return h('el-link', { 
          props: { underline: false },
          on: { click: () => { handle.editName(row) } }
        }, `我是${row.name}`)
      }
    }
  } }
])

columnConfig动态变化的时候,需要重新渲染表格,因此需要暴露一个reRender方法:

data () {
  return {
    isRender: true
  }
},
methods: {
  // 重新渲染
  reRender () {
    this.isRender = false
    this.$nextTick(() => {
      this.isRender = true
    })
  }
},
render (h) {
  ...
  return this.isRender ? table : null
}

至此,一个基本能满足所有需求的tableRender就好了,但是随着业务不断的重复,我们发现了至少两个问题:

  1. 通过h函数渲染dom写法非常繁琐
  2. table-column的处理事件或变量需要从组件传入配置文件内,维护起来也不是特别方便

针对以上两个问题,我们的解决思路是🧐:

  1. 能否让tableRender的scopedSlots也支持jsx???
  2. 能否获取页面作用域插槽v-slot内容,通过useSlot这个字段来控制渲染页面作用域插槽v-slot内容还是渲染配置文件自定义传入scopedSlots内容

类似如下:

// 配置文件:
const nameColumn = {
  field: 'name',
  title: '名称',
  minWidth: 200,
  scopedSlots: {
    default: ({ row }, h) => {
      return <div>{row.name}-jsx</div>
    },
    header: ({ column }, h) => {
      return (
        <div>
          <span>{column.label}</span>
          <el-tag type="success">测试下表头jsx</el-tag>
        </div>
      )
    }
  }
}
const operation = {
  label: '操作',
  prop: 'oprate',
  minWidth: 80,
  fixed: 'right',
  useSlot: true // 使用页面自定义插槽
}

// 页面:
<table-render :table-props="tableProps" :column-config="tableColumn">
  <!-- header插槽的使用 -->
  <template #header-operate="{ column }">
    <span>{{ column.label }}</span>
    <el-tooltip placement="top" content="自定义表头插槽" effect="light">
      <i class="el-icon-question"></i>
    </el-tooltip>
  </template>
  <!-- default插槽的使用 -->
  <template #operate="{ row }">
    <el-button type="text" @click="test(row.id)">测试按钮</el-button>
  </template>
</table-render>

3.png

支持渲染页面作用域插槽v-slot

拿到页面内插槽,可以通过this.slotsthis.slots或this.scopedSlots,这里应该是使用作用域插槽$scopedSlots

我们对render进行改造:

const columns = this.columnConfig.map((col) => {
  const { useSlot, scopedSlots, ...props } = col
  const defaultScope = this.$scopedSlots?.[props.prop]
  const headerScope = this.$scopedSlots?.[`header-${props.prop}`]
  const options = {
    props: props,
    scopedSlots: null
  }
  // 如果配置项中useSlot为true
  if (useSlot) {
    options.scopedSlots = {
      default: scope => defaultScope(scope),
      header: scope => headerScope(scope)
    }
  } else {
    // 如果scopedSlots为函数
    if (isFunction(scopedSlots)) {
      options.scopedSlots = scopedSlots(h)
    } else if (isObject(scopedSlots)) { // 如果scopedSlots为普通对象
      options.scopedSlots = {
        default: scope => scopedSlots.default(scope, h),
        header: scope => scopedSlots.header(scope, h)
      }
    }
  }
  retutn this.$createElement('el-table-column', options)
})

这里有朋友就问了为什么只写default和header两个插槽呢,哈哈因为el-table只提供了两个

4.png

支持多插槽多表格

当然,有些表格是有其他插槽的,所以这里我们是不能写死default和header两个插槽,因此我们把可能出现的插槽采用枚举的形式展现:

// table的插槽
const TABLE_SCOPED_SLOT = {
  DEFAULT: 'default',
  HEADER: 'header'
}
// table的插槽别名(前缀)
const TABLE_SCOPED_SLOT_ALIAS = {
  [TABLE_SCOPED_SLOT.DEFAULT]: '',
  [TABLE_SCOPED_SLOT.HEADER]: 'header-'
}
// 表格 插槽ScopedSlot 数组
const tableScopedSlot = [
  TABLE_SCOPED_SLOT.DEFAULT,
  TABLE_SCOPED_SLOT.HEADER
]

我们将获取页面作用域插槽获取由配置文件自定义传入scopedSlots抽离成两个方法

/**
  * @description: 获取页面作用域插槽v-slot内容
  * @param {Object} col 配置的列信息
  * @return {Object | null}
  */
getScopedSlotsByTemplate (col) {
  const scopedSlotsObj = {}
  for (let i = 0; i < tableScopedSlot.length; i++) {
    const key = tableScopedSlot[i]
    const customSlotName = `${TABLE_SCOPED_SLOT_ALIAS[key]}${col[this.prop]}`
    const customScope = this.$scopedSlots?.[customSlotName]
    customScope && (scopedSlotsObj[key] = scope => customScope(scope))
  }
  return isEmpty(scopedSlotsObj) ? null : scopedSlotsObj
}

/**
  * @description: 获取由配置文件自定义传入scopedSlots内容
  * @param {Object | Function} scopedSlots
  * @param {Function} h createElement
  * @return {Object | null}
  */
getScopedSlotsByConfig (scopedSlots, h) {
  let scopedSlotsObj = {}
  if (isFunction(scopedSlots)) scopedSlotsObj = scopedSlots(h)
  if (isObject(scopedSlots)) {
    for (let i = 0; i < tableScopedSlot.length; i++) {
      const key = tableScopedSlot[i]
      const customScope = _.get(scopedSlots, key) // lodash的get方法
      customScope && (scopedSlotsObj[key] = scope => scopedSlots[key](scope, h))
    }
  }
  if (scopedSlots && !isFunction(scopedSlots) && !isObject(scopedSlots)) {
    console.warn('[scopedSlots] 只支持Function和普通Object')
  }
  return isEmpty(scopedSlotsObj) ? null : scopedSlotsObj
}

至此,我们兼容了表格所有的插槽,同样,我们也可以兼容不同的表格:

// config.js
const tableCompoentMapping = {
  'el-table': {
    table: 'el-table',
    tableColumn: 'el-table-column',
    prop: 'prop',
    label: 'label'
  }
}

// tableRender.vue
export default {
  props: {
    tableType: {
      default: 'el-table',
      validator: type => {
        const success = typeof tableCompoentMapping[type] !== 'undefined'
        !success &&
          console.warn(`[table-render] tableType 必须为 ${Object.keys(tableCompoentMapping).join('、')} 中的一个`)

        return success
      }
    },
    ...
  },
  computed: {
    tableComponent () {
      return tableCompoentMapping[this.tableType] ?? {}
    },
    prop () {
      return this.tableComponent?.prop
    },
    label () {
      return this.tableComponent?.label
    },
    ...
  },
  render (h) {
    const columns = this.columnConfig.map((col, index) => {
      ...
      const options = ...
      return this.$createElement(this.tableComponent.tableColumn, options)
    })
    const table = this.$createElement(this.tableComponent.table, ..., columns)
    return ....
  }
}

回到开头提到的,其实随着需求越来越多,我们常常会写很多重复的代码,哈哈,这就是为什么需要封装思想了,无论是组件还是方法,或是类,都是朝着对自己更便利的方式走去

思考

对于tableRender组件,我们还有可以优化的地方吗?

这里我想到的是:表格的高度自适应

下一篇,我们讲述关于tableRender自适应问题:# tableRender自适应高度

相关代码:GitHub