1. 项目概述:用dnd-kit实现表格行拖拽排序
最近在重构后台管理系统时,遇到一个经典需求:让表格行支持拖拽排序。这个功能在数据分类管理、优先级调整等场景特别实用。经过技术选型,最终选择了dnd-kit这个现代拖拽库来实现。相比传统的react-dnd,dnd-kit的API更加简洁,性能优化做得更好,特别适合这种轻量级的交互场景。
先看最终效果:用户可以通过鼠标拖拽任意表格行,实时看到行位置变化,松开鼠标后自动完成数据排序更新。整个过程流畅自然,还支持触摸屏操作。实现这个功能主要涉及三个技术点:dnd-kit的核心API使用、表格行DOM结构处理、以及排序后的数据同步机制。
2. 技术选型与核心概念
2.1 为什么选择dnd-kit
在React生态中,实现拖拽功能主要有三个选择:react-dnd、react-beautiful-dnd和dnd-kit。经过对比测试:
- react-dnd:功能最强大但学习曲线陡峭,适合复杂拖拽场景
- react-beautiful-dnd:专为列表设计但体积较大
- dnd-kit:模块化设计,Gzip后仅10kb左右,API直观易用
对于表格排序这种相对简单的需求,dnd-kit是最佳选择。它提供了DndContext作为拖拽上下文,useDraggable和useDroppable两个核心Hook分别处理拖拽源和放置目标,以及各种传感器(Sensor)来适配不同输入设备。
2.2 拖拽排序的核心原理
实现表格行拖拽排序的本质是:
- 为每行添加拖拽手柄(drag handle)
- 定义可拖拽区域(draggable)和可放置区域(droppable)
- 监听拖拽事件并实时更新行位置
- 拖拽结束后同步数据到状态管理
dnd-kit通过Transform策略来处理元素位置变化,默认使用CSS transform来实现平滑的动画效果,这也是它性能优异的原因之一。
3. 完整实现步骤
3.1 基础环境搭建
首先安装必要依赖:
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities然后创建基础表格组件:
import { DndContext } from '@dnd-kit/core'; import { SortableContext, arrayMove } from '@dnd-kit/sortable'; function SortableTable({ data, onSort }) { return ( <DndContext onDragEnd={handleDragEnd}> <table> <SortableContext items={data}> {/* 表格内容 */} </SortableContext> </table> </DndContext> ); }3.2 实现可拖拽行
创建SortableRow组件:
import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; function SortableRow({ id, children }) { const { attributes, listeners, setNodeRef, transform, transition, } = useSortable({ id }); const style = { transform: CSS.Transform.toString(transform), transition, }; return ( <tr ref={setNodeRef} style={style} {...attributes}> <td> <button {...listeners}>≡</button> </td> {children} </tr> ); }关键点说明:
- useSortable Hook提供了拖拽所需的所有属性和方法
- listeners需要绑定到拖拽手柄(这里使用三横线按钮)
- transform和transition用于实现平滑的拖拽动画
- setNodeRef用于关联DOM元素
3.3 处理拖拽事件
完善DndContext的事件处理:
function handleDragEnd(event) { const { active, over } = event; if (active.id !== over?.id) { setItems((items) => { const oldIndex = items.findIndex(item => item.id === active.id); const newIndex = items.findIndex(item => item.id === over?.id); return arrayMove(items, oldIndex, newIndex); }); } }arrayMove是dnd-kit提供的工具函数,可以方便地实现数组元素位置交换。
4. 高级优化与功能增强
4.1 添加视觉反馈
为了提升用户体验,可以添加拖拽时的视觉反馈:
// 在SortableRow组件中添加 const { isDragging } = useSortable({ id }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, background: isDragging ? '#f0f0f0' : 'transparent', };4.2 限制拖拽方向
默认情况下dnd-kit允许任意方向拖拽,对于表格行排序,我们只需要垂直方向:
<DndContext modifiers={[restrictToVerticalAxis]} // ... >4.3 触摸屏适配
dnd-kit默认支持触摸屏操作,但需要配置PointerSensor和TouchSensor:
import { PointerSensor, TouchSensor } from '@dnd-kit/core'; const sensors = [ useSensor(PointerSensor), useSensor(TouchSensor), ]; <DndContext sensors={sensors}>5. 性能优化实践
5.1 减少不必要的渲染
使用React.memo优化行组件:
const MemoizedRow = React.memo(SortableRow);5.2 虚拟滚动支持
对于大型表格,结合react-window实现虚拟滚动:
import { FixedSizeList as List } from 'react-window'; function VirtualTable({ items }) { return ( <DndContext> <SortableContext items={items}> <List height={500} itemCount={items.length} itemSize={50} > {Row} </List> </SortableContext> </DndContext> ); }6. 常见问题与解决方案
6.1 拖拽时表格布局错乱
问题原因:拖拽时transform会影响元素尺寸计算 解决方案:为表格添加固定布局:
table { table-layout: fixed; width: 100%; }6.2 拖拽手柄不灵敏
问题原因:事件冒泡被阻止 解决方案:确保监听器绑定到足够大的区域:
<td> <div style={{ padding: '8px' }} {...listeners}> ≡ </div> </td>6.3 移动端无法触发拖拽
问题原因:触摸延迟 解决方案:添加touch-action样式:
.drag-handle { touch-action: none; }7. 完整代码示例
以下是完整的表格拖拽排序实现:
import React, { useState } from 'react'; import { DndContext, closestCenter, PointerSensor, TouchSensor, useSensor, useSensors, restrictToVerticalAxis, } from '@dnd-kit/core'; import { SortableContext, arrayMove, verticalListSortingStrategy, useSortable, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; const initialData = [ { id: 1, name: '项目A' }, { id: 2, name: '项目B' }, // 更多数据... ]; function SortableTable() { const [items, setItems] = useState(initialData); const sensors = useSensors( useSensor(PointerSensor), useSensor(TouchSensor) ); function handleDragEnd(event) { const { active, over } = event; if (active.id !== over?.id) { setItems((items) => { const oldIndex = items.findIndex(item => item.id === active.id); const newIndex = items.findIndex(item => item.id === over?.id); return arrayMove(items, oldIndex, newIndex); }); } } return ( <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd} modifiers={[restrictToVerticalAxis]} > <table> <thead> <tr> <th style={{ width: '50px' }}></th> <th>名称</th> </tr> </thead> <tbody> <SortableContext items={items} strategy={verticalListSortingStrategy} > {items.map((item) => ( <SortableRow key={item.id} id={item.id}> <td>{item.name}</td> </SortableRow> ))} </SortableContext> </tbody> </table> </DndContext> ); } function SortableRow({ id, children }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, background: isDragging ? '#f0f0f0' : 'transparent', }; return ( <tr ref={setNodeRef} style={style} {...attributes}> <td> <button {...listeners} style={{ cursor: 'grab', touchAction: 'none' }} > ≡ </button> </td> {children} </tr> ); }8. 扩展思考
在实际项目中,我们还可以进一步扩展这个功能:
- 多级嵌套排序:支持表格行分组和组内排序
- 跨表格拖拽:允许在不同表格间移动行
- 拖拽动画定制:使用Framer Motion实现更丰富的动画效果
- 撤销/重做功能:记录排序历史以便回退
dnd-kit的模块化设计让这些高级功能成为可能。比如要实现跨表格拖拽,只需要在DndContext中共享相同的context即可。