记录dnd-kit的使用
背景
最近在遇到了一个场景,类似于将一个便签拖拽到日历上,日历展示使用的antd。以前拖拽功能都是使用react-dnd,感觉不太好用。在询问了AI后推荐了dnd-kit。
| 维度 | dnd-kit | react-dnd |
|---|---|---|
| 底层实现 | 完全自研(鼠标/触摸/键盘事件) | HTML5 Drag & Drop API + 回退 |
| 包体积(gzip) | 核心 ≈ 10 kB,可按需拆包 | 最小组合 ≈ 25 kB |
| 学习曲线 | 3 个 Hook 即可跑通;API 扁平 | 概念多:Backend、Monitor、Connector、Collector |
| 组件侵入性 | 零额外 DOM,直接复用现有节点 | 必须套 DragSource/DropTarget 高阶组件 |
| 移动端 | 内置触摸传感器,开箱即用 | 需再装 react-dnd-touch-backend 并自己配 |
| 键盘/无障碍 | 官方提供完整键盘排序、Aria 示例 | 几乎要自己实现 |
| 碰撞检测 | 5 种内置算法,可自定义 | 无,需要手动计算坐标 |
| 拖拽镜像 | <DragOverlay> 一键置顶 | 需手写 dragPreview 或绝对定位节点 |
| 多容器/跨列表 | 官方示例直接支持 | 要写大量 Monitor 逻辑 |
| 虚拟滚动 | 官方文档给出虚拟列表方案 | 社区方案,集成复杂 |
| 维护 & 社区 | 2024 仍在快速迭代,现代化 | 稳定但迭代慢,API 基本冻结 |
| 典型场景 | 看板、日历、自由拖放、虚拟列表 | 传统两栏互拖、文件上传 |
dnd-kit以前用过,在antd 5版本中,列表拖拽用到的就是dnd-kit,但是场景固定的情况下,官方提供的代码是足够用的。它的原理和使用过程也不了解。所以借助这个场景,将dnd-kit的基本使用记录下来。
dnd-kit
dnd-kit通过拆包的方式让dnd-kit只用自己需要的,如希望有排序就安装@dnd-kit/sortable,限制拖拽边界就安装@dnd-kit/modifiers等,极大的节省空间。
| 子包 | 职责 | 何时引入 |
|---|---|---|
| core | 事件分发、碰撞检测、传感器 | 永远必需 |
| sortable | 排序策略、数组交换逻辑 | 做看板 / 列表排序时 |
| modifiers | restrictToVertical、snapToGrid 等边界修饰 | 需要锁轴、限界时 |
| utilities | CSS 工具函数、坐标计算辅助 | 写自定义传感器 / 动画时 |
| accessibility | 屏幕阅读器实时区域、键盘快捷键预设 | 要无障碍支持时 |
核心API
DndContext:整个拖拽舞台
useDraggable:让元素“可抓”
useDroppable:让区域“可放”
DragOverlay:顶层镜像,解决“被压住”问题
只需要一个核心就能解决大部分的问题
import { DndContext, DragOverlay } from '@dnd-kit/core';
export default function Demo() {
const [activeId, setActiveId] = useState<string | null>(null);
const handleDragStart = (e: DragStartEvent) => {
setActiveId(e.active.id as string); // 记录是谁被拖
};
const handleDragEnd = (e: DragEndEvent) => {
if (!e.over) return;
console.log('把', e.active.id, '放到', e.over.id);
};
return (
<DndContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<DraggableCard id="card-1">卡片</DraggableCard>//拖拽层
<DroppableDay id="day-1">日期格</DroppableDay>//下落层
<DragOverlay>//仿制顶层
{activeId ? <OverlayCard id={activeId} /> : null}
</DragOverlay>
</DndContext>
);
}
import { useDraggable } from "@dnd-kit/core";
export default function DraggableCard() {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id:'draggable',
});
return (
<div ref={setNodeRef} {...attributes , ...listeners}>
内容.....
</div>
);
}
import { useDroppable } from "@dnd-kit/core";
import { Calendar } from "antd";
//日历中的每个单元格都要添加useDroppable,所以要把单元格单独抽出来。
function DayCell({ date }: { date: Dayjs }) {
const dateStr = date.format("YYYY-MM-DD");
const { setNodeRef, isOver } = useDroppable({ id: dateStr });
return (
<div
ref={setNodeRef}
style={{
height: "100%",
background: isOver ? "#f6ffed" : undefined,
border: isOver ? "1px solid #73d13d" : undefined,
}}
>
{/* 这里随便再放 antd 原来的内容 */}
<div
className="ant-picker-cell-inner"
style={{
width: "100%",
height: 48, // 或 minHeight: 48
lineHeight: "48px",
textAlign: "center",
background: isOver ? "#f6ffed" : undefined,
border: isOver ? "1px solid #73d13d" : undefined,
}}
>
{date.date()}
</div>
</div>
);
}
export default function DroppableDay() {
return ( <Calendar fullCellRender={(date) => <DayCell date={date} />} /> );
}
import { useResterStore } from "./store";
import styles from "./roster.module.scss";
function OverlayCard({ id }: { id: string }) {
const tempList = useResterStore.use.tempList();
const item = tempList.find((g) => g[0].templateId === id);
if (!item) return null;
return (
<div className={styles.OverlayCard}>
{item.map((i) => (
<div key={i.user.id} className={styles.name}>
{i.user.name}
</div>
))}
</div>
);
}
export default OverlayCard;
总结
dnd-kit在上手难度上还是比较容易上手的,能让开发者将注意力更多的集中在业务上。适合现代开发。