为了给表格加虚拟滚动而选了 vxe-table?也可以考虑下这个

年初写过一篇表格选型踩坑,后来又开源了一个 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"
/>

virtualscroll.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,
  },
]

dataIndexsorterfilters / onFiltercustomRenderfixedellipsis、多级表头的 children,还有受控/非受控双轨(sortOrderdefaultSortOrder 都认),全是照着 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-wrapperant-table-theadant-table-cell 这些结构类,ant-table-small / ant-table-bordered 这种变体类,还有 ant-table-row-selectedant-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 的生态,这条能让表格看起来不违和;反过来也一样。

改样式不用打补丁

想改某个位置的样式,不用去猜类名然后写 !importantui 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 就能覆盖。

最后

相关推荐
打呵欠的猫33 分钟前
一个 Hook 让 AI 每次写文件前自动检查编码规范,违规代码再也提交不进来
前端·ai编程·代码规范
恋猫de小郭1 小时前
Android 17 + OkHttp 5.5.0 ,全新 ECH 下你的 HTTPS 域名可以请求时被安全隐藏
android·前端·flutter
后除1 小时前
从零到一: 创建一个 TypeScript 7 项目
前端·webpack·typescript
lerhxx1 小时前
我用 R3F 手搓了一个能走进去的 3D 迷宫简历(上):从选型架构到迷宫生成算法
前端·javascript·three.js
还有多久拿退休金1 小时前
不调多模态,纯文本大模型如何给系统操作配上截图
前端·llm·aigc
hunterandroid2 小时前
HarmonyOS WebSocket 实战:断线重连、心跳保活与连接状态机设计
前端
hunterandroid2 小时前
StateFlow 与 SharedFlow 的边界:状态与事件的正确建模
android·前端
心念科技3 小时前
1、搜索表单 xnSearch(基于:心念后台,后端 Java 21 + Spring Boot 4 + Spring Cloud,前端提供 ReactVue3+TS、Vue3+JS、Vue2+JS
前端
计算机魔术师3 小时前
Dwarkesh Patel 对 OpenAI/Hugging Face 事件的爆款解读被指危险误导
前端