背景
在日常中后台管理系统开发中,表格是使用频率最高的组件之一,当系统或者里面的页面很多时,封装一个表格组件很有必要。本文基于element-ui的table组件进行二次封装,让组件进一步解耦,达到精简代码,提高可读性。如果要在项目中使用,建议直接使用文末测试过的的优化版本。
封装目标
- api尽量和el-table一致,减少学习使用成本
- 传入一个表头的配置项,通过该配置项动态生成el-table-columns标签
- 支持自定义复杂表头
具体步骤
创建名为table的父组件,名为BaseTable的子组件。
在父组件中定义表头的配置项数据columns
,attrs
是表头属性,slot
代表这是个复杂的自定义表头,id
是作为循环的唯一key使用,如果直接用下表index,可能存在性能问题。代码如下:
<template>
<!-- 除了需要传columns,其它api与el-table完全一致 -->
<BaseTable v-loading="loading" :columns="columns" :data="list"></BaseTable>
</template>
<script>
import BaseTable from '@pages/common/BaseTable'
export default {
name: 'TabelDemo',
components: {
BaseTable
},
data() {
return {
loading: true,
list: [],
columns: Object.freeze([
{
attrs: {
prop: 'date',
label: '日期',
width: '150',
align: 'center'
},
id: 1
},
{
attrs: {
prop: 'author',
label: '作者',
width: '110',
'show-overflow-tooltip': true
},
id: 2
},
{
attrs: {
prop: 'des',
label: '简要描述',
'show-overflow-tooltip': true
},
id: 3
},
{
slot: 'handle',
attrs: {
label: '操作',
width: '230',
'class-name': 'small-padding fixed-width',
align: 'center'
},
id: 4
}
])
}
}
}
</script>
子组件中,在el-table组件上使用 a t t r s 和 attrs和 attrs和listeners,以$attrs为例,可以接收到父组件除了props已经接收的属性的其它所有属性,不包括class和style。官方说明如下:
包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class
和 style
除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class
和 style
除外),并且可以通过 v-bind="$attrs"
传入内部组件——在创建高级别的组件时非常有用。
官方的说明一看会很懵逼,其实简单理解就是你在父组件上绑定的属性(除了class
和 style
,以及在子组件中用props
已经接收的属性),使用
a
t
t
r
s
都
可
以
拿
到
。
然
后
在
当
前
子
组
件
上
用
‘
v
−
b
i
n
d
=
"
attrs都可以拿到。然后在当前子组件上用 `v-bind="
attrs都可以拿到。然后在当前子组件上用‘v−bind="attrs"`会把所有属性原样继承过来绑定上。
代码如下:
<template>
<!-- 通过v-bind="$attrs" v-on="$listeners",把父组件传的属性全部绑定到子组件上,保证了api和el-table一致 -->
<el-table style="width: 100%" v-bind="$attrs" v-on="$listeners">
<!-- 这里写coluumn-->
</el-table>
</template>
<script>
export default {
name: 'BaseTable',
props: {
columns: {
type: Array,
default: () => []
}
}
}
</script>
然后处理表头的渲染,columns中有slot属性的为复杂表头,同时为了在父组件中自定义内容,这里使用了具名插槽,同时绑定一个自定义的scope属性,把子组件里拿到的scope值传给父组件。
代码如下:
<el-table style="width: 100%" v-bind="$attrs" v-on="$listeners">
<template v-for="item in columns">
<el-table-column v-if="item.slot" :key="item.id" v-bind="item.attrs">
<template slot-scope="scope">
<slot :scope="scope" :name="item.slot"></slot>
</template>
</el-table-column>
<el-table-column v-else :key="item.id" v-bind="item.attrs"></el-table-column>
</template>
</el-table>
然后完善父组件数据进行测试
<template>
<!-- 除了需要传columns,其它api与el-table完全一致 -->
<BaseTable v-loading="loading" :columns="columns" :data="list" empty-text="哈哈哈,我就看看没数据会怎样~">
<template v-slot:handle="slot">
<el-button type="primary" size="mini" @click="handleUpdate(slot.scope.row, slot.scope.$index)">
修改
</el-button>
<el-button type="danger" size="mini" @click="handleDelete()">
清空
</el-button>
</template>
</BaseTable>
</template>
<script>
import BaseTable from '@pages/common/BaseTable'
export default {
name: 'TabelDemo',
components: {
BaseTable
},
data() {
return {
loading: true,
list: [],
columns: Object.freeze([
{
attrs: {
prop: 'date',
label: '日期',
width: '150',
align: 'center'
},
id: 1
},
{
attrs: {
prop: 'author',
label: '作者',
width: '110',
'show-overflow-tooltip': true
},
id: 2
},
{
attrs: {
prop: 'des',
label: '简要描述',
'show-overflow-tooltip': true
},
id: 3
},
{
slot: 'handle',
attrs: {
label: '操作',
width: '230',
'class-name': 'small-padding fixed-width',
align: 'center'
},
id: 4
}
])
}
},
created() {
setTimeout(() => {
this.list = [
{
date: '2020-10-13',
author: '南巢',
des: '我是南方来的燕啊,为何也会迷恋北方的寒。'
},
{
date: '2019-05-14',
author: '测试超出文本显示是否正常测试超出文本显示是否正常测试超出文本显示是否正常测试超出文本显示是否正常',
des: '我是南方来的燕啊,为何也会迷恋北方的寒。'
},
{
date: '2019-02-14',
author: '自卑感',
des: '低头瞥见自己的影子在前疯狂的跑着躲的离你不远沉默走的路不知几个光年我还原地打转连微笑也腼腆一事无成是最好描述要怎么往前'
}
]
this.loading = false
}, 1000)
},
methods: {
handleUpdate(row, index) {
console.log(row, index)
},
handleDelete() {
this.list = []
}
},
}
</script>
以为到这里就结束了吗,作为一个有追求的搬砖者,优化是必不可少的一节,直接贴优化后的代码:
父组件
<template>
<!-- 除了需要传columns,其它api与el-table完全一致 -->
<BaseTable v-loading="loading" :columns="columns" :data="list" empty-text="哈哈哈,我就看看没数据会怎样~">
<!-- slot就是接收到的子组件插槽里面的绑定的属性,可以任意命名,里面包含多条属性 -->
<!-- <template v-slot:handle="slot">
<el-button type="primary" size="mini" @click="handleUpdate(slot.scope.row, slot.scope.$index)">
修改
</el-button>
<el-button type="danger" size="mini" @click="handleDelete()">
清空
</el-button>
</template> -->
<!-- 下面是上面的简写,#是v-slot的简写,{scope: {row, $index}}是属性对象slot双重解构,注意这里的scope要与子组件插槽绑定的属性名对应 -->
<template #handle="{scope: {row, $index}}">
<el-button type="primary" size="mini" @click="handleUpdate(row, $index)">
修改
</el-button>
<el-button type="danger" size="mini" @click="handleDelete()">
清空
</el-button>
</template>
</BaseTable>
</template>
<script>
import BaseTable from '@pages/common/BaseTable' // 根据实际子组件路径引入
export default {
name: 'TabelDemo',
components: {
BaseTable
},
data() {
return {
loading: true,
list: [],
columns: Object.freeze([
{
attrs: {
prop: 'date',
label: '日期',
width: '150',
align: 'center'
},
id: 1
},
{
attrs: {
prop: 'author',
label: '作者',
width: '110',
'show-overflow-tooltip': true
},
id: 2
},
{
attrs: {
prop: 'des',
label: '简要描述',
'show-overflow-tooltip': true
},
id: 3
},
{
slot: 'handle',
attrs: {
label: '操作',
width: '230',
'class-name': 'small-padding fixed-width',
align: 'center'
},
id: 4
}
])
}
},
created() {
setTimeout(() => {
this.list = [
{
date: '2020-10-13',
author: '南巢',
des: '我是南方来的燕啊,为何也会迷恋北方的寒。'
},
{
date: '2019-05-14',
author: '测试超出文本显示是否正常测试超出文本显示是否正常测试超出文本显示是否正常测试超出文本显示是否正常',
des: '我是南方来的燕啊,为何也会迷恋北方的寒。'
},
{
date: '2019-02-14',
author: '自卑感',
des: '低头瞥见自己的影子在前疯狂的跑着躲的离你不远沉默走的路不知几个光年我还原地打转连微笑也腼腆一事无成是最好描述要怎么往前'
}
]
this.loading = false
}, 1000)
},
methods: {
handleUpdate(row, index) {
console.log(row, index)
},
handleDelete() {
this.list = []
}
},
}
</script>
子组件
<template>
<!-- 通过v-bind="$attrs" v-on="$listeners",把父组件传的属性全部绑定到子组件上,保证了api和el-table一致 -->
<el-table style="width: 100%" v-bind="$attrs" v-on="$listeners">
<!-- 考虑到v-for和v-if同时使用存在性能问题,这里直接使用computed把需要循环的数据过滤一遍,去除v-if -->
<!-- <template v-for="item in columns">
<el-table-column v-if="item.slot" :key="item.id" v-bind="item.attrs">
<template slot-scope="scope">
<slot :scope="scope" :name="item.slot"></slot>
</template>
</el-table-column>
<el-table-column v-else :key="item.id" v-bind="item.attrs"></el-table-column>
</template> -->
<el-table-column v-for="item in normalColumns" :key="item.id" v-bind="item.attrs"></el-table-column>
<el-table-column v-for="item in slotColumns" :key="item.id" v-bind="item.attrs">
<!-- vue2.6及以上版本,具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slot 和 slot-scope。
但考虑到element-ui官方文档依然使用的是slot-scope,这里不做更改。-->
<template slot-scope="scope">
<!-- :scope是绑定的动态属性,可以起任意喜欢的名字,但要注意在父组件中获取数据时key要对应 -->
<slot :scope="scope" :name="item.slot"></slot>
</template>
</el-table-column>
</el-table>
</template>
<script>
export default {
name: 'BaseTable',
props: {
columns: {
type: Array,
default: () => []
}
},
computed: {
// 获取普通的columns数据
normalColumns() {
return this.columns.filter(item => !item.slot)
},
// 获取是插槽的columns数据
slotColumns() {
return this.columns.filter(item => item.slot)
}
},
}
</script>
写在最后,按照最后优化的版本会存在一个问题,normalColumns数据肯定会在前面,slotColumns在后面,在项目使用时还是改为注释中的v-for嵌套v-if循环方式。
感谢各位老爷能看到这里,有什么不清楚或者写的不对的,欢迎留言指正~