剖析 table 组件

285 阅读7分钟

table 组件

由于这段时间在使用 antd 组件库, 使用 **table 组件 **始终一直有种 不够拿捏,不得心应手,一直以来 对 table 这个组件就使用起来就不够丝滑。这种感觉很难受啊。所以,干脆一次性向 table 这个组件结构 发起猛攻 !!今天之后,彻底 拿捏 table 。

220c09e7a30e3883d0e83fd3b2a39a5e.gif

原生的 table

先熟悉一下:

<table> 
    <thead>
      <tr>
        <th>姓名</th>
        <th>年龄</th>
        <th>性别</th>
        <th>职业</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th>周杰伦</th>
        <th>18</th>
        <th></th>
        <th>歌手</th>
      </tr>
      <tr>
        <th>邓紫棋</th>
        <th>18</th>
        <th></th>
        <th>歌手</th>
      </tr>
    </tbody>
</table>
// table : 表格区
// thead : 表头区
// tbody : 表格内容区
// tr    : 表行数据
// th    : 表列数据(单元格)

弄清楚概念

需要注意的就是:

  • 什么叫表头:表头就是一个表格的最上面一排,是每一列的 Title

  • 什么叫单元格:单元格就是表格的每一个,最小的单位,存放一个数据。

  • 什么是一行:一行就是一行,没什么好解释的。(一行就是包括了一组完整字段的数据,按一般的表格方向来看)

  • 表格方向:根据 表头的方向看,一般是行的方向。

    只需要清楚一般就是 数据横向展示的,即 一行就是 包含一条完整的实例的数据。(比如学生表,一行就包含了某个学生的 姓名,年龄,性别,班级 ...)这也是 符合常规认知,第一直觉的。

    详细来看的方向,指的是:

    1. 数据展示方向

      默认情况下,表格采用横向展示(行记录、列表头),即:

      • 每一行代表一条完整的数据记录(dataSource 中的一个对象)
      • 每一列对应数据对象的一个属性(columns 中定义的字段)

      如果需要纵向展示(列记录、行表头),即把行列颠倒(适合数据量少但字段多的场景),可以使用 tableLayout="fixed" 配合自定义渲染,或通过 List 组件模拟。

    2. 排列方向 (布局方向)

      主要指表格内容的文本排列方向整体布局方向,通过 direction 属性控制(针对国际化场景):

      • direction="ltr"(默认):左到右排列(适合中文、英文等)
      • direction="rtl":右到左排列(适合阿拉伯语、希伯来语等从右向左阅读的语言)

组件库中使用 table

Ant-design-vue 组件库(react 也有对应的 antd 组件库)

官网:Ant Design Vue — An enterprise-class UI components based on Ant Design and Vue.js

最重要的 参数

  • columns:表头配置对象

    columns 是一个数组,每一项就是一个 column配置对象,用于配置一个表头。常见的配置:

    插槽:作用就是,很多情况我们想 将表头 或 单元格的 内容设置的更加丰富,而不是简单的一个 字符串,一个数字;而是 自己定义里面的组件结构。这时就将表头或单元格的设置成一个作用域插槽,有名字,可以传参。

    插槽:

    • 可以设置在columns 的slots 配置中,title 表示 表头的插槽,customRender 表示单元格的插槽。(ts 不支持,js 可以)
    • column 中的 customRender 直接就可以接受一个 render函数。这样直接给 单元格设置组件结构。同理,column的 title 是否也可以接受一个 render函数呢?我想可以。(jsx/tsx的形式)
    • 总之:title 表示表头,customRender 表示单元格。

    插槽参数:固定的插槽参数有:

    • text:当前单元格的数据(对象或文本)(即 record[column.dataIndex])
    • record:当前行的完整的数据对象。
    • index:当前行的索引。
    • column:当前列的配置对象(即当前列的表头的配置对象),说白了就是,对于这一列的字段的配置。
    columns = [
        {
            title:''// 标头的标题(眼睛看到的)
            key:''// 该表头的唯一标识
            dataIndex:''// 该列的索引,和 key 一样就行。
            // ts 不能用这种方式,无法进行 插槽自定义
            ①.slots:{title:'表头的插槽',customRender:'在该表头下的单元格的插槽'}
            ②.customRender:()=>{}// render 函数
        }
    ]
    
  • dataSource:数据源

注意:record 是一整行的数据对象,他不一定里面所有的数据都要展示在 table 的字段里,**table字段只会展示 配置对象里面配置的字段。**所以说只是很灵活的,任何数据对象都可以展示在 table中,只要它里面包含了table 配置对象里面的字段。即 鸭子类型(Duck Typing)

动态类型语言中多态性的精髓 就是 “鸭子类型”。


table 组件内部渲染构建机制

我的问题:使用 插槽 bodycell时,table 的表头不止一个,为什么能直接结构出 column呢,难道是bodycell插槽内部有玄坤,比如循环每一个字段?

解答:实际上,Table组件内部在处理表格渲染时,会遍历每一列的配置,然后为每一列生成对应的单元格。对于自定义单元格内容,Table组件会将当前列的配置、当前行的数据、当前行的索引等信息通过作用域插槽传递给插槽内容。

具体来说,Table组件内部大致会做这样的处理:

  • 根据columns配置生成表头。
  • 遍历数据源dataSource的每一项(即每一行)。
  • 对于每一行,遍历columns配置的每一列。
  • 对于每一列,检查是否有自定义渲染(比如通过作用域插槽),如果有,则调用自定义渲染函数或使用作用域插槽,并传入当前列、当前行、当前行的索引等信息。

因此,我们在使用#bodyCell插槽时,实际上是在为每一个单元格提供自定义渲染。Table组件在遍历每一列的时候,会为每个单元格执行这个插槽,并将当前列的column信息传递进来。

所以,bodyCell插槽并不是只执行一次,而是对每行每列都会执行。在插槽内部,我们可以根据不同的column来做不同的渲染。


使用示例

config 配置对象:

const configTable = [
  {
    title: 'ID',
    dataIndex: 'id',
    width: 60,
    customRender: ({ record }: { record: BusItem }) => (
      <div class='text-sm text-secondary-200 code'>{record.id}</div>
    ),
  },
  {
    title: '班车时间',
    dataIndex: 'ctime',
    width: 150,
    customCell: () => ({
      class: 'text-sm',
    }),
  },
  {
    title: 'Tag',
    dataIndex: 'stage',
    width: 140,
    customRender: ({ record }: { record: BusItem }) => (
      <div class='text-sm'>
        {record.subTagId && (
          <Tag>
            <span class='text-secondary-200'>基准: </span>
            <span class='font-semibold'>{record.subTagId}</span>
          </Tag>
        )}
        {record.tagId && (
          <Tag>
            <span class='text-secondary-200'>发布: </span>
            <span class='font-semibold'>{record.tagId}</span>
          </Tag>
        )}
      </div>
    ),
  },
  {
    title: '更新时间',
    dataIndex: 'mtime',
    width: 150,
    customCell: () => ({
      class: 'text-sm',
    }),
  },
  {
    title: '操作',
    dataIndex: 'operatot',
    width: 140,
    fixed: 'right',
  },
] as any[];

使用 table 组件:

<Table
  rowKey="id"
  sticky
  size="small"
  :scroll="{ x: configTableWidth }"
  :loading="state.loading"
  :columns="configTable"
  :dataSource="state.data"
  :pagination="pagination"
>
  <template #bodyCell="{ column, record }">
    <template v-if="column.title === '操作'">
      <a class="text-blue-700 dib mr-2" @click="handleInfo(record.id)"
        >查看</a
      >
      <a
        class="text-blue-700 dib"
        v-if="
          [BUS_TYPE_AB, BUS_TYPE_NOAB].includes(record.busType) &&
          ((record.stage === 'ab' || record.status === 3) &&
            dayjs(record.ctime).isAfter(
              dayjs(dayjs().format('YYYY-MM-DD 00:00:00')),
            ))
        "
        @click="handleRerun(record.id)"
        >重发班车</a
      >
    </template>
  </template>
</Table>

这里的 bodyCell 插槽 完全可以写在 config titlie = '操作'的 所在列的 customRender字段中。

就像这样 :

{
  title: '操作',
  dataIndex: 'operator',
  width: 140,
  fixed: 'right',
  customRender: ({ record }) => {
    const actions = [];
    actions.push(
      <a 
        key="view" 
        className="text-blue-700 dib mr-2" 
        onClick={() => handleInfo(record.id)}
      >
        查看
      </a>
    );
    if (
      [BUS_TYPE_AB, BUS_TYPE_NOAB].includes(record.busType) &&
      ((record.stage === 'ab' || record.status === 3) &&
        dayjs(record.ctime).isAfter(dayjs(dayjs().format('YYYY-MM-DD 00:00:00'))))
    ) {
      actions.push(
        <a 
          key="rerun" 
          className="text-blue-700 dib" 
          onClick={() => handleRerun(record.id)}
        >
          重发班车
        </a>
      );
    }
    
    return <div>{actions}</div>;
  }
}

在 Ant Design Table 中,完全可以通过 customRender 属性来替代 bodyCell 插槽的功能,尤其是对于操作列这种需要自定义内容的场景,使用 customRender 会让代码结构更清晰,也更符合组件化的思想。

这种写法 ( 写在 config 里面 ) 的优势:

  1. 逻辑更内聚 - 列的配置和渲染逻辑放在一起,便于维护。
  2. 减少模板代码 - 不需要额外的 <template #bodyCell> 插槽。
  3. 更好的类型支持 - 在 TypeScript 中能获得更明确的类型提示。
  4. 更灵活的渲染控制 - 可以在 customRender 中进行更复杂的条件判断和元素组合。

好了,这下确实通透了很多了,真正能 达到 拿捏的 水平还需要多多在实战中打磨。平时容易忘没关系,小问题。用多了,踩了坑多了,印象深刻自然就深刻了。

正所谓,古人长云:“ 无他,唯手熟尔 ”,盖此章也 。