年初写过一篇表格选型踩坑,后来又开源了一个 Vue 3 表格组件------长得可以像 ant-design-vue,也可以像 element-plus。
开源这事有个常见的担心:发一篇文章热闹一下,然后就没下文了。
所以先交代进度。这几个月一直在不断迭代,最近这一轮主要是性能:纵向虚拟滚动的几个 O(n) 瓶颈清掉了,横向虚拟化补上了(几百列的宽表滚动不再卡),还做了一套 antdv 兼容类名------迁移的时候老 CSS 不用重写。
先说场景
数据量一上来,表格就开始卡。这事大家都遇到过。
然后你去翻文档,发现几条路都不太顺:
- 用
ant-design-vue的,a-table压根没有虚拟滚动,官方给的答案是分页 - 用
element-plus的,得换成el-table-v2,那是另一套 API、另一套列定义,多级表头和单元格合并还都不支持 - 于是很多人最后上了
vxe-table
vxe-table 确实能打,功能面也宽,这个我在选型那篇里就写过。但它有两个门槛让人犹豫。
一是心智要重来:列叫 VxeColumn,配置项、事件名、插槽全都得重新学一遍。为了一个虚拟滚动把整个表格层推倒重写,这笔账不是每个项目都划算。
二是使用它的时候,很大可能都要引入完整的vxe-pc-ui。
我做 vtable-guild 就是想在中间加一个选项:列定义还是 antdv 那套写法,虚拟滚动是内置的,依赖只有一个。
开箱就有的虚拟滚动
两个 prop,没了。
vue
<VTable
row-key="key"
:columns="columns"
:data-source="dataSource"
:scroll="{ y: 400 }"
:virtual="true"
/>
virtual 和 scroll.y 要一起给,只写 virtual 不给高度是不生效的------因为算不出视口能放几行。
十万行数据进来,DOM 里始终只有可视区的十来行。滚动的时候换掉这十来行的内容,不是把十万行都塞进文档里让浏览器自己扛。
有一点值得单独说:开了虚拟滚动,行高仍然可以不固定 。el-table-v2 必须定高;vxe-table 的类型注释里写明「启用纵向虚拟滚动之后将不能支持动态行高」,我在它最新的 4.21.2 上实测过:首屏行高是对的,滚动后新渲染的长文本行会被压回默认行高、内容裁剪------注释和行为一致,除非你用 setRowHeight 把每行高度自己算好喂给它。表格里有换行长文本、富文本单元格的话,这一条大概率能省掉你一堆折腾。
想让它更快也行,行高确定的话给个 rowHeight,走定高快路径。
列很多的那种表
行多是一回事,列多是另一回事。
ERP 的物料明细、BI 的宽表,动辄一两百列。这种表就算开了虚拟滚动照样卡------因为虚拟滚动只管行,列那个维度一个字没管。可见单元格数是「行数 × 列数」,十几行 × 200 列,还是两千多个单元格实打实待在 DOM 里。
所以又加了一个开关:
vue
<VTable
row-key="key"
:columns="columns"
:data-source="dataSource"
:scroll="{ x: 20000, y: 400 }"
:virtual="true"
:virtual-column="true"
/>
virtualColumn 只渲染视口里的那十几列,两端固定列照常钉住。200 列的表横着扫过去,渲染列数一直是十几个。
这个开关默认关着,也建议你保持关着,除非列数真的多。列不到五十的时候开它没有任何收益,纯属多做一次表头测量。
另外有几种情况它会自动回落到渲染全部列(dev 环境下控制台会告诉你原因):列上挂了 customCell / customRender(可能返回合并跨度,会破坏列宽对齐)、固定列没有分列两端、showHeader: false 且有非数字列宽(没有表头可量)。踩到了不会报错也不会错位,就是不优化而已。
还有一个已知边界:列滚出视口时,挂在该列表头上的筛选面板、tooltip 会跟着一起卸载。多数表格感知不到,但如果你的交互依赖「横向滚动时筛选面板保持打开」,先知道这条。
列宽倒是不用你操心,auto、百分比都行,宽度是从表头量出来的,不要求你写死数字。
列定义不用改
这是我最想说的一条。
从 antdv 迁过来,columns 基本一行不用动:
ts
const columns: TableColumnsType<UserRow> = [
{ title: '姓名', dataIndex: 'name', key: 'name', width: 180 },
{ title: '年龄', dataIndex: 'age', key: 'age', width: 96, align: 'right', sorter: true },
{
title: '状态',
dataIndex: 'status',
key: 'status',
filters: [
{ text: '在岗', value: 'active' },
{ text: '休假', value: 'paused' },
],
onFilter: (value, record) => record.status === value,
},
]
dataIndex、sorter、filters / onFilter、customRender、fixed、ellipsis、多级表头的 children,还有受控/非受控双轨(sortOrder 和 defaultSortOrder 都认),全是照着 antdv 抄的。
有一个地方不一样,得提前说:change 事件没有分页那个参数 ,签名是 (filters, sorter, extra)。因为这个库不内置分页,页码这块得你自己在页面层接。
老的覆盖 CSS 还能继续用
做过迁移的都知道,最烦的往往不是组件 API,是那堆积年累月写下来的样式补丁:
css
.ant-table-thead > tr > th {
background: #fafafa;
}
:deep(.ant-table-cell) {
padding: 8px 12px;
}
换个组件,这些全废了,得一条条重写。
现在可以不重写。全局开一个开关:
ts
app.use(
createVTableGuild({
compatClass: true, // 默认关闭
}),
)
开了之后,组件会在原有元素上额外挂一套 ant-table-* 类名------ant-table-wrapper、ant-table-thead、ant-table-cell 这些结构类,ant-table-small / ant-table-bordered 这种变体类,还有 ant-table-row-selected、ant-table-cell-fix-left-last 这类状态类都有。你那些老选择器就直接命中了。
DOM 结构不变,也不引入任何样式,就是多几个 class。开关前后我对着截图逐字节比过,像素没有变化。
有人可能会担心跟 antdv 自己的样式打架。一般不会:antdv 4.x 走 cssinjs,每条规则的选择器里都注入了 hash 类,实际长这样:
css
.ant-table-wrapper.css-dev-only-do-not-override-xxxxx .ant-table-thead > tr > th { ... }
我们的元素不带那个 hash,所以 antdv 的样式匹配不到我们,而你手写的选择器能正常命中------正好是想要的效果。我在同时加载 antdv 的页面上验过,178 条 ant-table 规则,命中我们元素的一条都没有。
唯一的例外是你用了 <StyleProvider :hashed="false">,那 antdv 就不生成 hash 了,选择器会退化成能匹配到我们的形式,这种情况别开这个开关。
顺带说明白:这是迁移期的过渡手段,不是我承诺的稳定 API。后面 DOM 结构调整了这些类名可能会变。建议当成「先跑起来」用,别当长期方案。
换一套皮
同一套 columns 和数据,改一行配置,整套视觉跟着换:
ts
app.use(createVTableGuild({ themePreset: 'element-plus' }))
表头、边框、行高、排序图标一起变,不用额外 import 任何 CSS。
项目里本来就是 element-plus 的生态,这条能让表格看起来不违和;反过来也一样。
改样式不用打补丁
想改某个位置的样式,不用去猜类名然后写 !important。ui prop 精确到 slot:
vue
<VTable :ui="{ th: 'bg-slate-100 text-slate-700', td: 'py-3' }" />
三层覆盖:默认主题 → 全局配置 → 实例 props,越往后优先级越高。全局统一规范,个别表格开例外,两件事不冲突。
CSS 变量那一层也开着,暗色模式之类的直接改 token 就行。
其他功能
一次性列完,都是常规能力,文档站每一项都有能直接点的 demo:
排序(多列、受控双轨)、筛选(多选 / 单选 / 树形 / 搜索 / 自定义面板)、行选择(checkbox / radio / 批量菜单 / checkStrictly)、展开行、树形数据、固定列、固定表头、多级表头、单元格合并、列宽拖拽、title / footer / summary、sticky、内置中英文 locale。
单元格编辑可以用 bodyCell 插槽自己组合,但没有内置的编辑状态和校验引擎。
什么时候别用它
说在前面,省得你集成到一半才发现:
- 要分页:没有内置,得自己接
- 要完整编辑引擎、Excel 导出、列拖拽换序、右键菜单:这些还是 vxe-table 的主场,它在这块的完备度我短期追不上
- 有无障碍合规要求:排序头和筛选触发器目前还不能键盘操作
- 就一张几十行的普通表 :原生
a-table/el-table够用,不用多引一个依赖
装一下
bash
pnpm add @vtable-guild/vtable-guild
ts
// main.ts
import { createVTableGuild } from '@vtable-guild/vtable-guild'
import '@vtable-guild/vtable-guild/css/style'
createApp(App).use(createVTableGuild()).mount('#app')
不需要装 Tailwind,也不强制装 antdv 或 element-plus。运行时依赖就一个 tailwind-variants,一个包一份样式,没有配套 UI 库要一起引。
项目里已经有 Tailwind 的话,把 cssMode 切到 tailwind3 / tailwind4,内部 class 就不带前缀,你直接写 px-2 就能覆盖。