如果我们经常使用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
}
支持传入配置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}`)
}
}
}
}
]
对于处理事件,直接从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就好了,但是随着业务不断的重复,我们发现了至少两个问题:
- 通过h函数渲染dom写法非常繁琐
- table-column的处理事件或变量需要从组件传入配置文件内,维护起来也不是特别方便
针对以上两个问题,我们的解决思路是🧐:
- 能否让tableRender的scopedSlots也支持jsx???
- 能否获取页面作用域插槽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>
支持渲染页面作用域插槽v-slot
拿到页面内插槽,可以通过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只提供了两个
支持多插槽多表格
当然,有些表格是有其他插槽的,所以这里我们是不能写死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