最近刚刚好有时间,看见了之间封装的饿了么的表格,感觉之前封装的方法说实在不太理想,于是就有了重构的想法,于是就这样开始了我的重构之旅。
项目环境
"vue": "^3.2.16",
"sass":"^1.43.4",
"typescript":"^4.4.3"
"vite"
复制代码
重构点
- JSX + JSON配置生成表格
- 支持插槽自定义表头,列表项
- 封装暴露的Api跟官方文档一致
- 支持分页
初始化
创建一个DynamicTable
组件,并且配置好需要由外部传入的配置项
<script lang="tsx">
import {defineComponent} from "vue";
import {isEmpty} from "element-plus/es/utils/util";
export default defineComponent({
name: "DynamicTable",
props: {
// 父组件的实例
parentDom: Object,
// 表格项
columns: {
type: Array,
default: () => ([])
},
// 表格的配置
options: {
type: Object,
default: () => ([])
},
// 操作按钮组
operations: {
type: Object,
default: () => ({})
},
// 表格的数据
tableData: {
type: Array,
default: () => ([])
},
// 分页配置
pagination:{
type:Object,
default:()=>({})
}
},
</script>
复制代码
可能有人会疑惑,就是parentDom
这个父组件实例有什么用?其实,它最主要的作用就是让当前的子组件可以直接调用父组件的方法,避免了使用emit
或者是直接将函数体传入配置所会产生的一些问题,而且因为我这里使用的是组合式APi
,在setup
中是无法获取到当前的this的。
如果你不明白,那么下面会慢慢说明
在开始之前,需要安装一下@vitejs/plugin-vue-jsx
这个插件,然后在vite.config.js
里面配置一下:
import {defineConfig} from 'vite'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
plugins: [
vueJsx({
// options are passed on to @vue/babel-plugin-jsx
}),
],
...
})
复制代码
JSX + JSON配置生成表格
首先我们上面定义了很多的prop,我们先来支持根据columns
,options
来配置和生成表格,并且将tableData
传入可以展示数据,上代码:
setup({columns, tableData}, ctx: any) {
// 删除掉要自定义的字段
const deleteField = (column: any) => {
delete column['align']
return column
}
return () => (
<div>
<el-table
data={tableData}
{...options}
>
{
columns.map((n: any) => {
const {align}: any = n
// 可以根据某一列自定义位置
const _align = align || options.align || 'center'
// 删除自定义的字段
deleteField(n)
return <el-table-column
align={_align}
{...n}
/>
})
}
</el-table>
</div>
)
}
复制代码
然后测试一下:
<DynamicTable
:columns="columns"
:tableData="tableData"
:options="options"
/>
const columns:Array = [
{label:'姓名',prop:'name',},
{label:'性别',prop:'sex'},
{label:'年龄', prop:'num',}
]
const options = {
border:true,
}
const tableData:Array<any> = [
{name:'1', sex:'男', num:12,},
{name:'2', sex:'男', num:12},
{name:'3', sex:'男', num:12},
]
复制代码
不出意外,他现在应该长下面这个样子
border
是官方的api,主要就是添加线条,那么到这里,我们最基础的功能就完成了,但是在实际的项目开发中,往往有这么几种需求:
- 需求1:表格的头部要自定义组件
- 需求2:表格的某一列要自定义组件
- 需求3:要求操作按钮可以根据当前行进行变化
- 需求4:表格的操作项需要可以自定义
- 需求5:表格中的数据需要根据后台返回的状态值得出对应的结果
所以!我们要继续完善我们的组件,来确保在大部分情况下,可以去适应大部分的需求。
添加组件对更多需求的支持
那么就上面的需求而言,个人最大的烦恼点,其实是数据应该如何交互
,比如说,我自定义了某个表单项column
,假如在其中定义了一个input
框,那么我们是不是可以考虑这种可能性,在我修改了input框
中的值之后,让表格中的数据同步变动,而不需要我们再去操作一次去修改。
当然上面只是一种可能性,还有比如下拉框
,时间选择器
等等这些可以随时变动数据的组件,都可以支持同步修改。那这个时候,就需要使用到引用传递这个东西了,关于引用传递大家应该都不陌生了,其实就是引用类型会发生的一个现象,那么我们传入的tableData
实际上保存在里面的是对象,就可以使用这个现象来对数据进行操作了。
那么一次性解决上面的需求吧
自定义头部 + 列内容 + 操作项
其实ElementUi官方已经提供了一个render-header
的api来支持自定义表头,可是本人呢,就是不喜欢使用JSX
或者是h
函数来做这些操作,因为在配置项中添加函数,他是没办法获取到当前的实例的,而且对于vue一些api支持也不够优雅,就比如v-model,在h
函数中 ,就需要自己自定义一次,关于这部分的内容官网也有说,有兴趣可以自己查找一下。那我们都用vue了,又有插槽这么一个好东西,所以我就干脆将所有的自定义操作
都修改成了支持插槽使用。
自定义头部 + 列内容
先来看一下我们希望怎么去使用这个自定义头部,也就是在代码中,如何编写能够节省最多的精力,不需要花费多余的时间去学习新的api,那肯定就是遵循官方文档的写法,而插槽普遍的使用方式如下:
<template v-slot:插槽名>
<template/>
复制代码
所以,我们希望当我们去定义一个头部或者是指定列内容的时候,它能够跟上面一样使用
<DynamicTable
:columns="columns"
:tableData="tableData"
:options="options"
>
// 自定义 name 这个column的表格头
<template v-slot:name_header>
<h2>我是name的表头</h2>
</template>
// 自定义 name 这个column的列内容
<template v-slot:name_content="slotProps">
<el-input v-model="slotProps.row.name" placeholder="Please input" />
</template>
</DynamicTable>
复制代码
那么为了完成可以支持这样的使用方式,我们可以使用一下slots这个变量,在setup中,它保存在ctx上下文中,然后修改的代码如下:
...
export default defineComponent({
name: "DynamicTable",
props: {
parentDom: Object,
// 表格项
columns: {
type: Array,
default: () => ([])
},
// 表格的配置
options: {
type: Object,
default: () => ([])
},
// 操作按钮组
operations: {
type: Object,
default: () => ({})
},
// 表格的数据
tableData: {
type: Array,
default: () => ([])
},
// 分页配置
pagination:{
type:Object,
default:()=>({})
}
},
setup({columns, tableData, options, operations, parentDom,pagination}, ctx: any) {
// 删除掉要自定义的字段
const deleteField = (column: any) => {
delete column['align']
return column
}
return () => (
<div>
<el-table
data={tableData} {...options}
>
{
columns.map((n: any) => {
const {align}: any = n
// 可以根据某一列自定义位置
const _align = align || options.align || 'center'
// 删除自定义的字段
deleteField(n)
// 定义插槽
const slots = {
default: ctx.slots[`${n.prop}_content`] ? ctx.slots[`${n.prop}_content`] : null,
header: ctx.slots[`${n.prop}_header`] ? ctx.slots[`${n.prop}_header`] : null
}
return <el-table-column
align={_align}
{...n}
v-slots={n.type === 'selection' ? null : slots}
/>
})
}
</el-table>
</div>
)
}
})
复制代码
然后测试一下,是否可以自定义表头,列表内容,并且可以自动修改表格项之中的内容,不出意外,现在的界面应该是
那么从右边可以看见,name这个列的第一项随着我们修改而被修改了,而表格的头部与内容也成功被我们用插槽自定义了。那么到此,我们总算是完成了一个表格最基础功能的百分之60。
从图片可以看见表格左边有多选的功能,那在官方的文档中,当我们多选的时候,可以提供一个函数,来接收当前被选到的行的数据,所以在我们的组件中也必须支持这个功能,那么这个时候,上面所有的parentDom
就很有用了,其实子组件触发父组件的函数并且传参的方式也有几种:
- 使用
Emit
来触发父组件的函数 - 在配置项中提供一个函数题以供调用
上面这两种方式算是最常见的使用方式了,可是他们在这种组件中使用,会产生不同的问题。
- emit 方式:
使用这个方式,就需要往组件上添加绑定的函数,这就导致写起来不够优化,而且不止需要在组件上绑定函数,还需要在配置项中将需要使用的对应的函数名传入组件中以供调用,那么就多了几步操作,个人不喜欢这样,所以pass掉。
- 在配置项中提供一个函数体
这个方式如下
...
const options = {
name:{
onClick:()=>{
在这里调用或传入父组件的方法
}
}
}
复制代码
这个方式的弊端更明显,首先是当前的函数体内的this并不是父组件的实例,所以在onClick中如果想要使用到父组件的数据,那么必须在DynamicTable
组件中获取到其$parent
,然后回传到onClick函数中,才能调用,多余的传参不说,假如我们在Dialog
中使用我们的组件的时候,其$parent
获取到的压根不是我们想要的那个实例,所以到这个时候就要使用
const options = {
name:{
onClick:(vm)=>{
// 在这里调用或传入父组件的方法
vm.$parent.$parent
}
}
}
复制代码
这样就很蛋疼了,又难看,又违背了简单方便使用的想法,所以最后,我采用了第三种方式,虽然也有一些问题,比如说将大批量的数据传入了另外一个组件,但是对于我们的实际使用上来说,其实不值一提。
- 直接将父组件的实例传入子组件
最后采用的是这种方案,既可以解决多余的调用的问题,又可以很方便的调用父组件中的方法,还不用担心函数中this的问题。
采用这种方案,需要调用函数的时候,只要在配置项中传入对应触发事件的函数名,那么我们这里还是遵循尽量跟官方文档一致的api这个想法来做,以完成多选功能
来完善我们的代码
...
setup({columns, tableData, options, operations, parentDom,pagination}, ctx: any) {
// 删除掉要自定义的字段
const deleteField = (column: any) => {
delete column['align']
return column
}
// 专门拿来触发父组件的函数
// handleName:函数名
// params:传入的参数
const handleFn = (handleName:string,params:any = null) => {
return parentDom && parentDom[handleName] && parentDom[handleName](params)
}
return () => (
<div>
<el-table
data={tableData} {...options}
onSelectionChange={(selection: any) => handleFn(options['selection-change'],selection)}
>
{
columns.map((n: any) => {
const {align}: any = n
// 可以根据某一列自定义位置
const _align = align || options.align || 'center'
// 删除自定义的字段
deleteField(n)
// 定义插槽
const slots = {
default: ctx.slots[`${n.prop}_content`] ? ctx.slots[`${n.prop}_content`] : null,
header: ctx.slots[`${n.prop}_header`] ? ctx.slots[`${n.prop}_header`] : null
}
return <el-table-column
align={_align}
{...n}
v-slots={n.type === 'selection' ? null : slots}
/>
})
}
{renderOperations()}
</el-table>
{ !isEmpty(pagination) && renderPagination()}
</div>
)
}
复制代码
其实就是当发现选项是selection
的时候,就不需要el-table-column
最任何操作,但是也要支持原来el-table-column
的api,所以在渲染插槽的时候添加一下判断就好了,使用方式就是在columns
中定义一个type:selection
的对象就行了。
const columns = [
{type:'selection'}
]
复制代码
那因为时间的关系,上文就先到这里啦,余下的操作项
,分页
其实按照以上的思路也不难做了,而这里,就在之后的下文中再详细说明吧!
告辞