ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Vuejs复杂表格实战:基于vuedatagrid打造高性能可定制数据表格

Vuejs复杂表格实战:基于vuedatagrid打造高性能可定制数据表格 简介vue-data-grid 是一个基于 Vue.js 构建复杂可交互表格的完整示例项目面向有一定 Vue 基础、希望系统掌握组件化表格开发的读者适合用于学习排序、过滤、分页、自定义单元格渲染等常见需求。资源共 48 个文件压缩包仅 85KB包含 17 个 JS 文件、10 个 Vue 文件、7 个 SVG 图标、3 个 Sass 样式以及 JSON 配置、HTML 入口、README 文档和构建相关脚本其中 Vue 单文件组件分别负责表格整体、行、列、表头、分页等模块SVG 图标与 Sass 样式便于自定义视觉主题。目录按源码、配置、静态资源组织结构清晰便于直接运行与二次扩展。示例围绕 vue-data-grid 的交互机制展开讲解了通过 v-on 监听用户点击、输入事件后由 Vue 数据绑定更新数据模型实现列头排序、关键词过滤、分页计算同时演示了利用插槽自定义单元格内容、通过 axios 与后端 API 异步加载数据以及与 Vuex、Vue Router 配合使用的思路。已有 2728 人学习下载适合作为 Vue.js 中级开发者实现复杂表格的参考范本。1. 项目背景为什么我选择了自研数据表格方案先交代一下背景。这个项目的起因是业务方提了一个很常见但又很棘手的需求需要一个支持排序、筛选、行内编辑、批量操作、分页、固定表头、多选联动还要能处理几万行数据的表格页面。当时团队里有人提议直接用现成的UI库表格组件也有人建议上vuedatagrid这类专门的数据表格库但调研一圈下来发现都不够贴合我们的场景——要么交互定制成本太高要么性能在数据量上来之后撑不住。vuedatagrid当时给我的印象是它不是一个开箱即用的傻瓜组件而是一套基于Vuejs的表格解决方案骨架。它把表格拆成了数据层、渲染层、交互层三个维度你可以在它的基础上做深度定制而不是被锁死在某个UI框架的API里。这个设计理念正好踩中我们的需求点——我们需要的是“能改造成业务专属表格”的地基而不是一个功能固定的大全套。这篇文章我不会去复述vuedatagrid的官方文档而是从一个实际项目的角度把整个表格从零到一搭建的过程、踩过的坑、优化思路和最终效果完整记录下来。如果你也在用Vuejs做复杂表格或者正被表格性能、交互复杂度折磨这篇内容应该能帮你少走不少弯路。2. 整体设计思路表格不只是“一堆列和数据”2.1 先拆需求复杂表格到底复杂在哪里动手写代码之前我把业务需求拆成了几个维度这里也建议你拿到类似需求时先做这一步千万别上来就写模板。我们的表格需求拆完是这样的数据维度单表最大1万行以上列数30列左右部分列是动态渲染的列显隐可配置。交互维度行内编辑、批量删除、单选/多选、跨页选中、排序、筛选、列宽拖拽、固定列。状态维度单元格编辑态、校验失败态、数据加载态、空数据态、选中态这些状态要能互相切换且不能互相干扰。性能维度滚动不能卡顿输入框输入不能有明显延迟排序筛选操作要在500ms内出结果。拆完之后你会发现表格的复杂度80%不在“渲染表格”本身而在状态管理和数据流转。vuedatagrid的价值在于它帮你把表格区域划分成了清晰的模块表头配置、数据源、单元格渲染器、交互事件总线。你可以往每个模块里塞自己的业务逻辑而不用和组件内部的DOM渲染逻辑纠缠。2.2 为什么选vuedatagrid而不是从零手写有人可能会问既然都要深度定制为什么不干脆自己写一套表格我的回答是自己写可以但成本差异巨大。从零手写一套支持上述所有能力的表格光是虚拟滚动、表头固定、列宽拖拽这三个功能就需要投入数周时间去处理边界情况更别提排序筛选的联动逻辑。而vuedatagrid已经把表格的骨架和基础交互搭好了你要做的核心事情是两件第一把业务数据结构映射到vuedatagrid的配置模型上。这个映射层决定了你后续所有定制能力是否能顺滑地施展。第二在它的生命周期钩子和事件接口上追加业务逻辑。比如编辑完某个单元格后要联动计算合计值选中一行后要同步更新详情面板这些逻辑放对位置代码就不会变成一坨乱麻。用一句大白话总结我的选型思路vuedatagrid提供的是“表格的半成品”业务方需要的是“成品”中间那层“深加工”正是项目的核心工作也是这篇文章要重点展开的部分。2.3 目录结构和模块划分整个表格模块我按下面这种方式组织这种划分方式是项目跑起来之后验证过比较顺手的src/views/data-grid/ ├── config/ # 表格配置列定义、交互开关、默认状态 │ ├── columns.js │ └── gridOptions.js ├── components/ # 自定义单元格渲染器和编辑器 │ ├── CellStatus.vue │ ├── CellEditorInput.vue │ └── CellActions.vue ├── composables/ # 业务逻辑复用层 │ ├── useGridData.js │ ├── useGridSelection.js │ └── useGridFilter.js ├── data/ # 模拟数据或接口请求层 │ └── mockData.js └── index.vue # 页面入口组装以上所有模块这种拆法的好处是表格配置和数据逻辑分离以后换接口、加列、改交互模式基本只需要动对应模块不用在页面组件里大海捞针。下面我按这个目录顺序把每个环节的关键实现讲透。3. 核心细节解析列配置、数据绑定与单元格定制3.1 列配置模型把“列”变成一个可维护的配置对象vuedatagrid的列配置核心是一个字段数组每个字段描述一列的属性。我强烈建议你在项目里把每个列做成一个配置对象而不是散落在模板里。一个实际的列配置大概是这样的// config/columns.js export const columns [ { field: orderNo, label: 订单号, width: 160, sortable: true, fixed: left, formatter: (row) row.orderNo || - }, { field: status, label: 状态, width: 120, sortable: true, // 自定义单元格渲染后面会讲 render: CellStatus }, { field: amount, label: 金额, width: 140, sortable: true, align: right, formatter: (row) ¥ ${Number(row.amount).toFixed(2)} }, { field: operation, label: 操作, width: 200, fixed: right, // 不绑定数据字段直接渲染操作按钮 render: CellActions } ]这里有个细节值得注意formatter和render是两个不同层级的能力。formatter适合做纯文本格式化比如金额加币种符号、日期格式转换它不涉及DOM操作性能开销小。render则用来挂载自定义Vue组件适合需要交互的单元格比如状态标签、编辑框、按钮组。我踩过的坑是一开始把复杂交互全部塞进formatter函数里写模板字符串结果数据更新时DOM无法局部更新滚动起来明显卡顿。后来统一改成组件渲染问题才解决。原则就是——纯展示用formatter有交互用组件这个边界越清晰后续维护越省心。3.2 数据绑定和响应式处理避免“改了一个单元格整张表重绘”vuedatagrid的数据源是一个数组通常直接绑定一个响应式数据。但复杂表格场景下一个核心问题是如果你把整个表格数据都放进一个Vue的reactive对象里任何一行数据的变动都可能触发大范围的响应式更新。这里分享一个实测有效的处理方式表格的基础数据源用markRaw或者普通数组 手动版本号控制避免Vue对每一行做深度响应式代理。只有需要参与界面状态联动的字段比如选中态、编辑态才放进响应式对象。用Object.freeze冻结纯展示的静态数据行减少代理开销。当初我处理1万行数据时对比过效果全量响应式代理的情况下滚动帧率大概30fps左右有明显的掉帧感。改成普通数组 局部状态对象后帧率能稳定在55fps以上体感差距非常大。但要注意一个边界如果你完全不用响应式那单元格内部的变化比如编辑状态的切换就不会自动驱动视图更新。我的做法是划分“数据层”和“视图状态层”数据层保持轻量视图状态层一个存着{ rowIndex, colField, editing }的Map用Vue的reactive管理。这样既保证交互流畅又不会丢失响应式的便利。3.3 自定义单元格组件把业务渲染拆出去vuedatagrid的一个很实用的能力是支持注册自定义单元格渲染器。你可以把状态标签、编辑输入框、操作按钮全部封装成独立组件然后在列配置里通过组件名引用// config/columns.js import CellStatus from ../components/CellStatus.vue import CellActions from ../components/CellActions.vue import CellEditorInput from ../components/CellEditorInput.vue // 在组件中注册 export default { components: { CellStatus, CellActions, CellEditorInput } }每个自定义单元格组件接收的行数据、列字段等信息由vuedatagrid注入你只需要在组件里声明props即可。拿状态标签为例!-- components/CellStatus.vue -- template span classcell-status :classstatusClass {{ statusText }} /span /template script setup import { computed } from vue const props defineProps({ row: { type: Object, required: true }, field: { type: String, required: true } }) const statusMap { pending: { text: 待处理, class: is-pending }, processing: { text: 处理中, class: is-processing }, done: { text: 已完成, class: is-done } } const statusClass computed(() statusMap[props.row.status]?.class || ) const statusText computed(() statusMap[props.row.status]?.text || 未知) /script这种做法的好处是列配置和渲染逻辑完全解耦你要新增一种状态展示只需要写一个新组件然后改列配置不用动表格主体逻辑。团队其他人接手时也只需要看components目录就能理解每一格是怎么渲染的。4. 实操过程从零搭一个带编辑、排序、筛选的完整例子4.1 项目初始化和基础表格渲染我这里直接用一个Vite Vue3工程举例项目初始化步骤就不展开了直接进入表格核心部分。首先是安装vuedatagrid这里以npm方式安装为例npm install vuedatagrid然后页面入口组件里最基础的用法是这样!-- index.vue -- template div classdata-grid-page div classtoolbar button clickhandleBatchDelete批量删除/button button clickhandleExport导出当前数据/button span classselection-info已选 {{ selectedRows.length }} 项/span /div VueDataGrid :columnscolumns :datagridData :optionsgridOptions selection-changehandleSelectionChange sort-changehandleSortChange filter-changehandleFilterChange refgridRef / /div /template script setup import { ref, reactive, onMounted } from vue import VueDataGrid from vuedatagrid import { columns } from ./config/columns import { gridOptions } from ./config/gridOptions import { fetchGridData } from ./data/mockData import { useGridData } from ./composables/useGridData import { useGridSelection } from ./composables/useGridSelection const gridRef ref(null) const { gridData, loadData, updateCell } useGridData() const { selectedRows, handleSelectionChange } useGridSelection() const gridOptions reactive({ height: 600, rowHeight: 44, headerRowHeight: 48, fixedHeader: true, virtualScroll: true, multiSelect: true, selectOnRowClick: false, sortable: true, filterable: true, pagination: { enabled: true, pageSize: 50, pageSizeOptions: [20, 50, 100] }, emptyText: 暂无数据试试调整筛选条件 }) onMounted(async () { await loadData() }) /script这里把gridOptions单独拎出来放到配置文件里管理是因为这个对象里的配置项非常多如果全部堆在页面里后期改一个参数要翻半天模板。单独抽出去之后所有表格的“全局行为”都集中在了一个文件里维护成本低很多。4.2 实现行内编辑从“展示态”到“编辑态”的无缝切换行内编辑是复杂表格里最常见也最容易做崩的交互。vuedatagrid本身不会直接给你一个editable: true就完事它需要你组合自定义单元格组件和事件机制来落地。我的实现思路是每个可编辑单元格维护一个展示/编辑双态双击进入编辑态失焦或回车保存。实现方式是在单元格组件里用editing状态控制展示和编辑的切换!-- components/CellEditorInput.vue -- template div classcell-editor dblclickstartEdit span v-if!editing classcell-display{{ displayValue }}/span input v-else refinputRef v-modeleditValue classcell-input blurcommitEdit keyup.entercommitEdit keyup.esccancelEdit / /div /template script setup import { ref, computed, nextTick } from vue const props defineProps({ row: { type: Object, required: true }, field: { type: String, required: true } }) const emit defineEmits([update-cell, edit-complete]) const editing ref(false) const editValue ref() const inputRef ref(null) const displayValue computed(() { return props.row[props.field] ?? - }) async function startEdit() { editValue.value displayValue.value editing.value true await nextTick() inputRef.value?.focus() inputRef.value?.select() } function commitEdit() { if (!editing.value) return const oldValue displayValue.value const newValue editValue.value if (oldValue ! newValue) { emit(update-cell, { row: props.row, field: props.field, value: newValue }) } editing.value false } function cancelEdit() { editing.value false editValue.value displayValue.value } /script这里有几个实际项目中必须注意的细节。第一个是blur和keyup.enter同时监听时回车触发提交后input会失焦有可能导致commitEdit被调用两次。解决办法是在commitEdit开头加一个if (!editing.value) return的守卫实测就不会有重复提交的问题。第二个细节是编辑态输入框默认高度和单元格高度不一致会导致跳动。我在实际项目里让输入框高度撑满单元格同时用box-sizing: border-box确保边框不会挤占内容空间这样切换起来视觉上基本无感。第三个细节是日期、下拉选择这类特殊编辑器的处理。我的建议是不要试图在通用编辑组件里塞下所有类型而是每种编辑器独立成组件在列配置里指定editor: CellEditorDate、editor: CellEditorSelect这样的方式做分发。一开始图省事做一个万能编辑器结果后面每个类型都在互相影响改一处崩两处得不偿失。4.3 排序和筛选联动不只是调接口排序和筛选看起来是两个独立功能但在业务场景里它们经常要叠加使用先筛选出特定状态的数据再按金额排序。vuedatagrid对排序和筛选都有内置支持但默认的排序算法只处理基础类型。实际项目里我处理金额、日期这类字段排序时需要自定义排序函数// config/gridOptions.js export const gridOptions { // ...其他配置 sort: { defaultSort: { field: orderNo, order: asc }, // 覆盖内置排序按数字处理器排序 compare: (a, b, field, order) { const va getFieldValue(a, field) const vb getFieldValue(b, field) if (field amount) { const na parseFloat(va) || 0 const nb parseFloat(vb) || 0 return order asc ? na - nb : nb - na } if (field createdAt) { const ta new Date(va).getTime() const tb new Date(vb).getTime() return order asc ? ta - tb : tb - ta } return String(va).localeCompare(String(vb), zh-CN) } } }筛选的联动逻辑我在项目里的处理方式是本地筛选。数据量在1万行以内时本地筛选的性能完全足够没必要每次筛选都请求后端。前端把筛选条件收敛到一个状态对象里然后用计算属性输出过滤后的数据传给表格// composables/useGridFilter.js import { ref, computed } from vue export function useGridFilter(originData) { const filterConditions ref({}) function handleFilterChange({ field, value }) { filterConditions.value[field] value } const filteredData computed(() { const conditions filterConditions.value const hasConditions Object.keys(conditions).length 0 if (!hasConditions) return originData.value return originData.value.filter((row) { return Object.entries(conditions).every(([field, condition]) { const cellValue row[field] if (condition ) return true // 支持模糊匹配 return String(cellValue).toLowerCase().includes( String(condition).toLowerCase() ) }) }) }) return { filterConditions, filteredData, handleFilterChange } }这里有一个容易忽略的点筛选后如果用户又勾选了某些行选中集合里可能会混入已经被筛掉的行。解决办法是选中状态始终保存行的唯一标识比如订单号、id页面展示选中数时用“选中且在当前筛选结果的集合内”的数量避免出现“选了10行界面上只看到3行”这种让用户以为出bug的情况。4.4 虚拟滚动和性能优化让1万行数据不卡顿大数据的核心瓶颈在DOM节点数量。1万行如果全部渲染成真实DOM哪怕每行只有10个节点页面里也有10万个节点浏览器直接吃不消。vuedatagrid提供的虚拟滚动能力在这里非常关键——它只渲染可视区域内出现的行滚动时动态替换渲染内容。实际使用中虚拟滚动要生效有几个前提条件表格容器必须有固定高度不能是auto。每一行的高度最好统一这样计算滚动位置才精确。如果行高不一致虚拟滚动的容器高度计算会出错。不要对每一行做批量响应式监听参考前面的做法把静态行数据冻结起来。我实测的一个性能对比数据可以作为参考2500行数据、每行12个单元格关闭虚拟滚动时首屏渲染时间约1.2秒开启后降到约120毫秒滚动时掉帧情况基本消失。如果你的表格数据量到了几万行虚拟滚动不是“可选项”而是“必须项”。另外还有一个优化技巧把不参与排序和筛选的列设为不参与计算。排序筛选的性能瓶颈主要在对每一行做字段值提取列越多越慢。在列配置里显式声明sortable: false、filterable: false的字段会走更快的跳过逻辑这在字段多的时候能省下不少计算。4.5 跨页选中和批量操作一个必须提前设计的细节批量操作在分页场景下有个经典难题用户在第1页选了3行翻到第2页又选了2行最后点删除时应该删5行而不是只删当前页的2行。这个问题如果等到开发后期再处理改动成本会很大所以我在这里单独拎出来讲讲建议一开始就设计好。我的做法是用useGridSelection这个组合式函数统一管理选中状态// composables/useGridSelection.js import { ref, computed } from vue export function useGridSelection() { // 用 Set 存储选中行的唯一id天然去重且顺序稳定 const selectedIds ref(new Set()) function handleSelectionChange(rows) { // vuedatagrid 会传入当前页选中的行数组 // 但我们要维护的是跨页的唯一集合所以这里只更新标识 if (Array.isArray(rows)) { const ids rows.map((row) row.id) selectedIds.value new Set([ ...Array.from(selectedIds.value), ...ids ]) } } // 暴露给外部使用的选中行数据 const selectedRows computed(() { return Array.from(selectedIds.value).map((id) { // 通过id从数据源里反查行数据避免直接依赖当前的渲染数据 return idMap.value.get(id) }).filter(Boolean) }) function clearSelection() { selectedIds.value new Set() } return { selectedIds, selectedRows, handleSelectionChange, clearSelection } }这个方案的要点是永远保存的是选中项的id集合而不是选中行的引用。因为行数据在筛选、排序、翻页过程中可能被重新创建或移动只有id是稳定的。删除操作完成后记得从Set里过滤掉已删除的id否则会残留脏数据。5. 常见问题与排查技巧实录5.1 表格数据更新了但视图没刷新这是Vue表格类项目里最常被问的问题八成原因是数据结构的响应式依赖没触发。排查步骤我分享一套先确认是“数据没变”还是“视图没变”用console.log打印绑定表格的数据源。如果数据源变了但视图没动看是不是给数组直接赋值了下标比如rows[0].name x这种在Vue3里用reactive包裹的数组是可以触发的但如果你的数据源是用普通数组 markRaw方式管理的就不会触发。解决办法是更新数据时创建一个新数组替换比如gridData.value [...gridData.value]强制表格感知到变化。5.2 虚拟滚动时行内组件状态错乱自定义单元格组件在虚拟滚动下会被复用因为滚动过程中组件不销毁只是绑定的数据变了。如果组件内部用了非响应式的局部状态就可能出现“上一行的状态残留到下一行”的bug。我遇到的一个典型案例是编辑输入框的editing状态在滚动后没有重置导致滚动到其他行时某一格直接进入了编辑态。解决办法是在组件里监听row或field的变化重置内部状态watch( () [props.row, props.field], () { editing.value false } )类似的逻辑在自定义组件里都要考虑到凡是依赖“当前行数据”的内部状态都需要在数据变化时重置。5.3 表格初始化时闪现“暂无数据”如果接口请求需要时间表格初始化后暂时没有数据会先渲染空状态然后数据返回后再更新。如果接口慢用户会看到空状态在闪体验很不好。我的处理方式是在配置里加一个loading状态通过gridOptions或插槽显示加载动效同时在数据返回前不渲染空状态提示。具体做法是const loading ref(true) watch(loading, (val) { if (val) { gridOptions.loadingText 数据加载中... } else { gridOptions.loadingText } })5.4 表格数据和外部状态不同步还有一种常见问题表格内部维护了一份数据组件外部也维护了一份数据两边因为某个操作没同步导致显示不一致。比如在表格里编辑了金额但页面顶部的合计值没有更新。解决思路是尽量把表格的数据源作为“唯一数据源”任何编辑操作都通过事件通知到父组件由父组件统一更新数据再回传给表格。不要出现“表格自己改一份、父组件再改一份”的双写模式。我之前在项目里用update-cell事件统一做数据流转再配合一个总合计的计算属性数据一致性就再没出过问题。5.5 常见问题速查表问题现象可能原因处理办法滚动卡顿严重未开启虚拟滚动或行高不固定检查gridOptions配置统一行高编辑后保存的数据丢了只改了本地state没通知父组件通过emit事件统一走数据流转排序结果不对字符串排序被用于数字字段自定义compare函数按字段类型处理筛选后选中行数异常选中保存的是行对象而非id改用id Set存储选中状态自定义组件跨行复用状态错乱组件内部状态未随数据重置watch row/field变化时重置状态表格高度自适应导致虚拟滚动失效容器高度为auto给容器设置固定高度或监听resize后更新高度数据量大时初始化慢大量行数据被响应式代理用markRaw或Object.freeze冻结纯展示数据6. 写在最后的实操心得这个项目做下来最大的感受是表格组件的选型只是第一步真正的工程量在于把业务逻辑和表格能力之间的缝隙用代码填平。vuedatagrid给我带来的价值是它把“表格渲染”这个通用能力做扎实了让我可以把精力全部放在业务定制上而不是和DOM渲染、滚动计算这些底层细节死磕。最后再分享一个我后来养成的习惯任何表格项目开工前先花一天时间把列配置、数据模型、交互状态列成一张Excel表和产品对一遍。表格的需求边界如果不理清后面每加一个交互都要动表格核心代码那种痛苦经历过一次就再也不想经历了。如果你正准备在Vuejs项目里上复杂表格我的建议是别被“复杂”两个字吓住按列配置、数据绑定、自定义单元格、交互事件、性能优化这几条主线拆开来做每一步都有章可循整个项目就没那么可怕了。表格这活儿做得多了你会发现核心思路永远是相通的。本文还有配套的精品资源点击获取
返回列表