拖拽排序与拖拽交互
场景
实现列表拖拽排序或类似 Trello 的看板拖拽交互。
实现方案
原生 HTML Drag and Drop API
基础列表拖拽排序
function DraggableList({ items, onReorder }: Props) {
const [dragIndex, setDragIndex] = useState<number | null>(null);
const handleDragStart = (index: number) => (e: React.DragEvent) => {
setDragIndex(index);
e.dataTransfer.effectAllowed = 'move';
};
const handleDragOver = (index: number) => (e: React.DragEvent) => {
e.preventDefault(); // 必须,否则不允许 drop
if (dragIndex === null || dragIndex === index) return;
const newItems = [...items];
const [removed] = newItems.splice(dragIndex, 1);
newItems.splice(index, 0, removed);
onReorder(newItems);
setDragIndex(index);
};
return (
<ul>
{items.map((item, i) => (
<li
key={item.id}
draggable
onDragStart={handleDragStart(i)}
onDragOver={handleDragOver(i)}
onDragEnd={() => setDragIndex(null)}
style={{ opacity: dragIndex === i ? 0.5 : 1 }}
>
{item.name}
</li>
))}
</ul>
);
}
移动端触摸拖拽
原生 DnD API 在移动端支持有限,需要用 Touch 事件实现:
Touch 拖拽核心逻辑
function useTouchDrag(onDragEnd: (from: number, to: number) => void) {
const dragInfo = useRef<{ startIndex: number; currentIndex: number } | null>(null);
const handleTouchStart = (index: number) => (e: React.TouchEvent) => {
dragInfo.current = { startIndex: index, currentIndex: index };
};
const handleTouchMove = (e: React.TouchEvent) => {
if (!dragInfo.current) return;
const touch = e.touches[0];
const element = document.elementFromPoint(touch.clientX, touch.clientY);
const index = element?.getAttribute('data-index');
if (index !== null && index !== undefined) {
dragInfo.current.currentIndex = Number(index);
}
};
const handleTouchEnd = () => {
if (!dragInfo.current) return;
const { startIndex, currentIndex } = dragInfo.current;
if (startIndex !== currentIndex) {
onDragEnd(startIndex, currentIndex);
}
dragInfo.current = null;
};
return { handleTouchStart, handleTouchMove, handleTouchEnd };
}
推荐库
| 库 | 特点 |
|---|---|
@dnd-kit/core | 现代、模块化、支持多种交互 |
react-beautiful-dnd | Atlassian 出品,列表排序体验好(已停维护) |
SortableJS | 框架无关,功能强大 |
常见面试问题
Q1: 拖拽排序的核心原理?
答案:
dragstart→ 记录拖拽项索引dragover→ 更新位置(交换数组元素),preventDefault()允许放置drop / dragend→ 完成排序,更新状态- 移动端用
touchstart/touchmove/touchend+document.elementFromPoint实现相同逻辑