【VXE-Table】+【中后台大数据表格】:从列配置、单元格合并到虚拟滚动,一站式掌握实战配置规范,避开卡顿、错乱、合并失效高频坑!
本文基于 VXE-Table 4.x 编写,部分 API 在不同小版本中可能有差异,请以项目实际使用的版本文档为准。
VXE-Table 官方文档

📑 文章目录
- [一、开篇:为什么要单独讲 VXE-Table?](#一、开篇:为什么要单独讲 VXE-Table?)
- [二、列配置(Column Config):日常怎么选、怎么配](#二、列配置(Column Config):日常怎么选、怎么配)
- [2.1 列配置的两种写法](#2.1 列配置的两种写法)
- [2.2 核心字段速查表](#2.2 核心字段速查表)
- [2.3 固定列(fixed)规范与坑](#2.3 固定列(fixed)规范与坑)
- [2.4 columnConfig:表格级别的列行为](#2.4 columnConfig:表格级别的列行为)
- [三、合并单元格:mergeCells 与 spanMethod 怎么选](#三、合并单元格:mergeCells 与 spanMethod 怎么选)
- [3.1 两种方式对比(必读)](#3.1 两种方式对比(必读))
- [3.2 mergeCells 规范写法](#3.2 mergeCells 规范写法)
- [3.3 spanMethod 写法(适用于动态合并逻辑)](#3.3 spanMethod 写法(适用于动态合并逻辑))
- [3.4 合并单元格的常见坑](#3.4 合并单元格的常见坑)
- 四、虚拟滚动:什么时候开、怎么开、有哪些限制
- [4.1 虚拟滚动的本质](#4.1 虚拟滚动的本质)
- [4.2 怎么启用虚拟滚动](#4.2 怎么启用虚拟滚动)
- [4.3 旧版 API 与新版的区别](#4.3 旧版 API 与新版的区别)
- [4.4 虚拟滚动的限制(避坑必读)](#4.4 虚拟滚动的限制(避坑必读))
- [4.5 完整示例:带虚拟滚动的表格](#4.5 完整示例:带虚拟滚动的表格)
- [五、综合实战:列配置 + 合并 + 虚拟滚动](#五、综合实战:列配置 + 合并 + 虚拟滚动)
- 六、避坑速查表
- 七、小结
- [🔍 系列模块导航](#🔍 系列模块导航)
同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。
(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)
很多前端开发者都会遇到一个瓶颈:
代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。
想写出干净、优雅、可维护 的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验。
这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。
帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。
一、开篇:为什么要单独讲 VXE-Table?
业务里但凡遇到「数据量大、列多、要合并、要虚拟滚动」的表格,Element Plus 的 el-table 常常不够用。VXE-Table 是专门为此设计的 Vue 表格方案,但 API 多、约束也多,一旦配置不当就会卡顿、空白、合并失效。
本文聚焦三个核心能力:列配置 、合并单元格 、虚拟滚动,按「怎么配 → 为什么这么配 → 常见坑」的结构讲清楚,方便你直接照着写、照着避坑。
[⬆ 返回目录](#⬆ 返回目录)
二、列配置(Column Config):日常怎么选、怎么配
2.1 列配置的两种写法
VXE-Table 支持两种列定义方式:
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
模板写法 <vxe-column> |
列固定、少量、交互简单 | 直观、好读 | 动态列要写很多 v-if |
配置写法 columns |
后端返列、动态显隐、列很多 | 灵活、易维护 | 要理解字段含义 |
规范建议:
- 列基本固定的 → 用模板;
- 列由后端配置或需要列设置、导出配置的 → 用
columns配置。
[⬆ 返回目录](#⬆ 返回目录)
2.2 核心字段速查表
js
// columns 数组里每列常见字段
const columns = [
{
type: 'seq', // 列类型:seq序号 | checkbox | radio | expand | html
field: 'id', // 字段名,对应 data 里的属性(越深性能越差,避免 a.b.c.d)
title: 'ID', // 列标题
width: 80, // 列宽,数字或 'auto'
minWidth: 60, // 最小宽度
resizable: true, // 是否可拖拽调整列宽
visible: true, // 是否显示(配合列设置)
fixed: 'left', // 固定:'left' | 'right' | ''(空字符串表示不固定)
align: 'center', // 对齐:'left' | 'center' | 'right'
sortable: true, // 是否可排序
filters: [...], // 筛选配置
formatter: ({ cellValue }) => cellValue, // 格式化显示
slots: { default: 'customSlot' }, // 插槽名
}
]
说明:
field越浅越好,a.b.c比a更耗性能。fixed必须是字符串'left'、'right'或'',不能是true/false(后面会讲坑)。
[⬆ 返回目录](#⬆ 返回目录)
2.3 固定列(fixed)规范与坑
正确写法:
html
<template>
<vxe-table :columns="columns" :data="tableData" />
</template>
<script setup>
const columns = [
{ field: 'name', title: '姓名', width: 120, fixed: 'left' },
{ field: 'age', title: '年龄', width: 80 },
{ field: 'address', title: '地址', width: 200 },
{ field: 'action', title: '操作', width: 150, fixed: 'right' }
]
</script>
常见坑 1:后端返回 true/false 导致样式错乱
js
// ❌ 错误:后端返回 boolean
const columns = res.data.columns.map(col => ({
...col,
fixed: col.fixed // 可能是 true,vxe-table 期望 'left'
}))
// ✅ 正确:统一转成字符串
const columns = res.data.columns.map(col => ({
...col,
fixed: col.fixed === true ? 'left' : (col.fixed === 'right' ? 'right' : '')
}))
常见坑 2:分组表头时 fixed 要设在 colgroup 上
html
<!-- ❌ 错误:在 column 上设 fixed,分组表头会错乱 -->
<vxe-colgroup title="基本信息">
<vxe-column field="name" title="姓名" fixed="left" />
<vxe-column field="age" title="年龄" fixed="left" />
</vxe-colgroup>
<!-- ✅ 正确:fixed 写在 colgroup 上 -->
<vxe-colgroup title="基本信息" fixed="left">
<vxe-column field="name" title="姓名" />
<vxe-column field="age" title="年龄" />
</vxe-colgroup>
规范结论:
- 固定列一定要用
'left'/'right'字符串; - 有分组表头时,
fixed配置在vxe-colgroup上。
[⬆ 返回目录](#⬆ 返回目录)
2.4 columnConfig:表格级别的列行为
html
<vxe-table
:column-config="{ resizable: true }"
:data="tableData"
>
常用配置:
| 属性 | 类型 | 说明 |
|---|---|---|
resizable |
Boolean | 是否允许拖拽调整列宽 |
useKey |
Boolean | 是否用 field 作为列唯一 key,动态列时建议开 |
[⬆ 返回目录](#⬆ 返回目录)
三、合并单元格:mergeCells 与 spanMethod 怎么选
3.1 两种方式对比(必读)
| 对比项 | mergeCells | spanMethod |
|---|---|---|
| 用法 | 配置数组,指定行号、列号、合并范围 | 函数,根据行列返回合并信息 |
| 虚拟滚动 | ✅ 支持(官方做了适配) | ❌ 跨行合并时纵向虚拟滚动不可用 |
| 大数据量 | ✅ 适合 | ❌ 容易卡顿 |
| 动态合并 | 需在数据变更后重新赋值 | 每次渲染都会执行函数 |
| 适用场景 | 固定规则、可计算的合并 | 复杂、高度依赖行列数据的合并 |
规范建议:
- 能用
mergeCells的,优先用mergeCells; - 只有「必须根据单元格内容动态决定合并」时,才用
spanMethod,并接受虚拟滚动受限。
[⬆ 返回目录](#⬆ 返回目录)
3.2 mergeCells 规范写法
基本结构:
js
// 每个合并项:从 (row, col) 开始,合并 rowspan 行、colspan 列
mergeCells: [
{ row: 0, col: 1, rowspan: 3, colspan: 1 }, // 第 0 行第 1 列,向下合并 3 行
{ row: 2, col: 2, rowspan: 1, colspan: 2 } // 第 2 行第 2 列,向右合并 2 列
]
完整示例:按部门合并「部门」列
html
<template>
<vxe-table
ref="tableRef"
:columns="columns"
:data="tableData"
:merge-cells="mergeCells"
/>
</template>
<script setup>
import { ref, computed } from 'vue'
const tableData = ref([
{ dept: '研发部', name: '张三', role: '前端' },
{ dept: '研发部', name: '李四', role: '后端' },
{ dept: '研发部', name: '王五', role: '测试' },
{ dept: '销售部', name: '赵六', role: '销售' },
{ dept: '销售部', name: '钱七', role: '销售' },
])
const columns = [
{ field: 'dept', title: '部门', width: 120 },
{ field: 'name', title: '姓名', width: 100 },
{ field: 'role', title: '角色', width: 100 },
]
// 根据数据计算合并:同一部门连续多行时,合并部门列
const mergeCells = computed(() => {
const list = tableData.value
const result = []
let startRow = 0
for (let i = 1; i <= list.length; i++) {
const needMerge = i < list.length && list[i].dept === list[i - 1].dept
if (!needMerge) {
const count = i - startRow
if (count > 1) {
result.push({ row: startRow, col: 0, rowspan: count, colspan: 1 })
}
startRow = i
}
}
return result
})
</script>
注意 :row、col 从 0 开始,对应数据行和 columns 的下标。
[⬆ 返回目录](#⬆ 返回目录)
3.3 spanMethod 写法(适用于动态合并逻辑)
js
// spanMethod 返回 { rowspan, colspan } 或 false(不合并)
const spanMethod = ({ row, rowIndex, column, columnIndex }) => {
if (column.field === 'dept') {
// 与上一行部门相同则被合并,不单独返回
if (rowIndex > 0 && tableData.value[rowIndex - 1].dept === row.dept) {
return { rowspan: 0, colspan: 0 } // 0 表示被上一行合并
}
const count = 计算连续相同部门数量(rowIndex)
return { rowspan: count, colspan: 1 }
}
return false
}
⚠️ 重要限制:
- 使用
spanMethod做跨行合并时,不能开启纵向虚拟滚动,否则会错乱或空白; - 数据量大时
spanMethod执行频繁,易卡顿。
[⬆ 返回目录](#⬆ 返回目录)
3.4 合并单元格的常见坑
坑 1:数据更新后合并没变
mergeCells 是「快照」,数据变化后要重新计算并赋值。若用 ref 存 mergeCells,记得在 tableData 更新后重新赋值:
js
watch(tableData, () => {
mergeCells.value = calcMergeCells(tableData.value)
}, { deep: true })
坑 2:用 Grid 时 setMergeCells 再次赋值不生效
在 VXE-Grid 等场景,setMergeCells() 可能在 columns 更新后被重置。解决方式:在数据加载完成的回调里用 nextTick 再设一次:
js
querySuccess: ({ response }) => {
tableData.value = response.items
nextTick(() => {
const merges = calcMergeCells(response.items)
gridApi.grid.setMergeCells(merges)
})
}
坑 3:合并 + 虚拟滚动 + 冻结列一起用
同时使用这三个功能,容易出现滚动卡顿、固定列留白。建议:
- 合并 + 虚拟滚动:可以,用
mergeCells; - 合并 + 冻结列:可以,但注意表格宽度;
- 三者同时:尽量避免,或降低数据量、减少合并范围。
[⬆ 返回目录](#⬆ 返回目录)
四、虚拟滚动:什么时候开、怎么开、有哪些限制
4.1 虚拟滚动的本质
只渲染可视区域内的行,其余行不渲染,从而在万级、十万级数据时仍能保持流畅。代价是:必须固定行高、固定表格高度,且部分高级能力不可用。
[⬆ 返回目录](#⬆ 返回目录)
4.2 怎么启用虚拟滚动
必要条件:
- 表格有固定高度:
height="400"或height="100%"(父容器有高度) - 行高固定:
row-config="{ height: 50 }"(虚拟滚动不支持动态行高)
基础配置示例(VXE-Table 4.x):
html
<template>
<vxe-table
height="500"
:scroll-y="{ enabled: true, gt: 20 }"
:row-config="{ height: 50 }"
:data="tableData"
>
<vxe-column type="seq" width="60" />
<vxe-column field="name" title="姓名" width="120" />
<vxe-column field="age" title="年龄" width="80" />
</vxe-table>
</template>
参数说明:
| 属性 | 说明 | 推荐值 |
|---|---|---|
enabled |
是否启用 | true |
gt |
数据行数超过此值才启用虚拟滚动 | 50~200 即可,避免小数据也开虚拟 |
[⬆ 返回目录](#⬆ 返回目录)
4.3 旧版 API 与新版的区别
不同版本可能看到不同写法,对照如下:
js
// 旧版(部分 3.x / 4.x 早期)
:optimization="{ scrollY: { gt: 200 } }"
// 新版(4.x 中后期)
:scroll-y="{ enabled: true, gt: 200 }"
以你项目中的文档为准,本文以 scroll-y 写法为主。
[⬆ 返回目录](#⬆ 返回目录)
4.4 虚拟滚动的限制(避坑必读)
| 限制 | 说明 |
|---|---|
| 不支持动态行高 | 必须用 row-config.height 固定行高 |
| 与 spanMethod 跨行合并冲突 | 跨行合并时不能开纵向虚拟滚动 |
| 树形、展开行 | 部分场景下表现异常,需实测 |
| 合并 + 虚拟 + 冻结列 | 三者同开易卡顿,能避免就避免 |
规范结论:
- 数据量 < 500:可不开虚拟滚动;
- 500~5000:建议开,
gt设 200~500;
5000:强烈建议开,且用 mergeCells 做合并,不要用 spanMethod。
[⬆ 返回目录](#⬆ 返回目录)
4.5 完整示例:带虚拟滚动的表格
html
<template>
<div class="table-wrap">
<vxe-table
ref="tableRef"
height="500"
:scroll-y="{ enabled: true, gt: 100 }"
:row-config="{ height: 48 }"
:column-config="{ resizable: true }"
:columns="columns"
:data="tableData"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const columns = [
{ type: 'seq', width: 60, fixed: 'left' },
{ field: 'name', title: '姓名', width: 120, fixed: 'left' },
{ field: 'dept', title: '部门', width: 150 },
{ field: 'role', title: '角色', width: 120 },
{ field: 'address', title: '地址', minWidth: 200 },
{ field: 'action', title: '操作', width: 120, fixed: 'right' },
]
const tableData = ref([])
onMounted(async () => {
const res = await fetch('/api/list')
const data = await res.json()
tableData.value = data.items
})
</script>
<style scoped>
.table-wrap {
height: 500px;
}
</style>
[⬆ 返回目录](#⬆ 返回目录)
五、综合实战:列配置 + 合并 + 虚拟滚动
下面是一个同时用到「动态列、合并部门列、虚拟滚动」的完整示例,可直接参考。
html
<template>
<div class="demo-table">
<vxe-table
ref="tableRef"
height="500"
:scroll-y="{ enabled: true, gt: 50 }"
:row-config="{ height: 48 }"
:column-config="{ resizable: true }"
:columns="columns"
:data="tableData"
:merge-cells="mergeCells"
/>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
// 列配置:姓名、部门左固定,操作右固定
const columns = ref([
{ type: 'seq', width: 60, fixed: 'left' },
{ field: 'name', title: '姓名', width: 100, fixed: 'left' },
{ field: 'dept', title: '部门', width: 120 },
{ field: 'role', title: '角色', width: 100 },
{ field: 'address', title: '地址', minWidth: 180 },
{ field: 'action', title: '操作', width: 120, fixed: 'right' },
])
const tableData = ref([])
// 合并部门列:同一部门连续多行合并
const mergeCells = computed(() => {
const list = tableData.value
const result = []
const deptColIndex = columns.value.findIndex(c => c.field === 'dept')
if (deptColIndex < 0) return result
let startRow = 0
for (let i = 1; i <= list.length; i++) {
const needMerge = i < list.length && list[i].dept === list[i - 1].dept
if (!needMerge) {
const count = i - startRow
if (count > 1) {
result.push({
row: startRow,
col: deptColIndex,
rowspan: count,
colspan: 1,
})
}
startRow = i
}
}
return result
})
onMounted(async () => {
// 模拟接口返回
const mockData = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `用户${i + 1}`,
dept: ['研发部', '销售部', '产品部'][i % 3],
role: ['前端', '后端', '测试', '销售'][i % 4],
address: `地址${i + 1}`,
action: '',
}))
tableData.value = mockData
})
</script>
<style scoped>
.demo-table {
padding: 16px;
height: 500px;
}
</style>
[⬆ 返回目录](#⬆ 返回目录)
六、避坑速查表
| 场景 | 推荐做法 | 避免 |
|---|---|---|
| 固定列 | fixed: 'left' / 'right' 字符串 |
使用 true/false |
| 分组表头 + 固定列 | 在 vxe-colgroup 上配置 fixed |
只在 vxe-column 上配置 |
| 合并单元格 | 优先 mergeCells |
大数据量用 spanMethod 跨行合并 |
| 虚拟滚动 | 设置 height、row-config.height |
动态行高、未设高度 |
| 合并 + 虚拟 | 用 mergeCells,控制合并范围 |
合并 + spanMethod + 虚拟 |
| 动态 mergeCells | 数据更新后重新计算并赋值,必要时 nextTick |
只赋一次值不更新 |
[⬆ 返回目录](#⬆ 返回目录)
七、小结
- 列配置 :分清模板和配置写法;
fixed用字符串;分组表头时fixed写在 colgroup 上。 - 合并单元格 :能
mergeCells就mergeCells;spanMethod只留给必须动态合并且数据量不大的场景。 - 虚拟滚动:必须固定高度和行高;大数据优先开,注意与合并、冻结列的兼容性。
按这些规范来配,大部分卡顿、错乱、合并失效问题都能避免。
[⬆ 返回目录](#⬆ 返回目录)
🔍 系列模块导航
📝 表单与表格规范篇
一、《Vue3 + Element Plus 表单开发实战:防重复提交、校验、重置、loading 统一|表单与表格规范篇》
二、《Vue3 + Element Plus 表单校验实战:规则复用、自定义校验、提示语统一,告别混乱避坑|表单与表格规范篇》
三、《Vue3 + Element Plus 表格查询规范:条件管理、分页联动 + 避坑,标准化写法|表单与表格规范篇》
四、《Vue3 + Element Plus 表格实战:批量操作、行内编辑、跨页选中逻辑统一|表单与表格规范篇》
五、《VXE-Table 4.x 实战规范:列配置 + 合并单元格 + 虚拟滚动,避坑卡顿 / 错乱 / 合并失效|表单与表格规范篇》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
📚 系列总览
「前端规范实战系列 」正在持续更新中,后续会整理一篇《前端规范实战系列全系列目录导航》,包含每篇文章简介 + 直达链接,方便大家按顺序、体系化学习。
更新中,敬请期待~
[⬆ 返回目录](#⬆ 返回目录)
技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护。
哪怕每次只吃透一条规范,长期下来,差距会非常明显。
后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。
觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。
我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~