Vue3使用JSX封装JSON化ElementUi表格-上文

最近刚刚好有时间,看见了之间封装的饿了么的表格,感觉之前封装的方法说实在不太理想,于是就有了重构的想法,于是就这样开始了我的重构之旅。

项目环境

"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},
    ]
复制代码

不出意外,他现在应该长下面这个样子

image.png border是官方的api,主要就是添加线条,那么到这里,我们最基础的功能就完成了,但是在实际的项目开发中,往往有这么几种需求:

  • 需求1:表格的头部要自定义组件
  • 需求2:表格的某一列要自定义组件
  • 需求3:要求操作按钮可以根据当前行进行变化
  • 需求4:表格的操作项需要可以自定义
  • 需求5:表格中的数据需要根据后台返回的状态值得出对应的结果

所以!我们要继续完善我们的组件,来确保在大部分情况下,可以去适应大部分的需求。

添加组件对更多需求的支持

那么就上面的需求而言,个人最大的烦恼点,其实是数据应该如何交互,比如说,我自定义了某个表单项column,假如在其中定义了一个input框,那么我们是不是可以考虑这种可能性,在我修改了input框中的值之后,让表格中的数据同步变动,而不需要我们再去操作一次去修改。
当然上面只是一种可能性,还有比如下拉框,时间选择器等等这些可以随时变动数据的组件,都可以支持同步修改。那这个时候,就需要使用到引用传递这个东西了,关于引用传递大家应该都不陌生了,其实就是引用类型会发生的一个现象,那么我们传入的tableData实际上保存在里面的是对象,就可以使用这个现象来对数据进行操作了。
那么一次性解决上面的需求吧

自定义头部 + 列内容 + 操作项

其实ElementUi官方已经提供了一个render-header的api来支持自定义表头,可是本人呢,就是不喜欢使用JSX或者是h函数来做这些操作,因为在配置项中添加函数,他是没办法获取到当前的实例的,而且对于vue一些api支持也不够优雅,就比如v-model,在h函数中 ,就需要自己自定义一次,关于这部分的内容官网也有说,有兴趣可以自己查找一下。那我们都用vue了,又有插槽这么一个好东西,所以我就干脆将所有的自定义操作都修改成了支持插槽使用。

扫描二维码关注公众号,回复: 13653662 查看本文章

自定义头部 + 列内容

先来看一下我们希望怎么去使用这个自定义头部,也就是在代码中,如何编写能够节省最多的精力,不需要花费多余的时间去学习新的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>
    )
  }
})
复制代码

然后测试一下,是否可以自定义表头,列表内容,并且可以自动修改表格项之中的内容,不出意外,现在的界面应该是

image.png 那么从右边可以看见,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'}
]
复制代码

那因为时间的关系,上文就先到这里啦,余下的操作项,分页其实按照以上的思路也不难做了,而这里,就在之后的下文中再详细说明吧!
告辞

猜你喜欢

转载自juejin.im/post/7050700477259841567