ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

dnd-kit 拖拽库核心原理解析与实战:从原生拖拽到 React 排序

dnd-kit 拖拽库核心原理解析与实战:从原生拖拽到 React 排序 去年年末把一个内部后台项目从原生 HTML5 拖拽重构到 dnd-kit前后折腾了三周。期间翻遍了官方文档、源码和 issue 区踩了不少坑也把它的设计思路摸了个七七八八。这篇东西不打算写成 API 手册而是想沿着为什么需要它 → 核心抽象是怎么设计的 → 真实项目中怎么落地 → 哪些地方最容易翻车这条线聊聊我对 dnd-kit 的理解。无论你是正准备选型还是已经接入但被各种诡异行为折磨这篇应该都能给你一些参考。1. 为什么我从原生 HTML5 拖拽逃到了 dnd-kit先说结论HTML5 原生拖拽 API 不是不能用但当你做的不是把 A 拖到 B这种最小 demo而是真正的复杂业务交互时它的问题会一个接一个冒出来直到你不得不换方案。1.1 原生 API 的四大硬伤第一个硬伤是拖拽预览。默认情况下浏览器会把被拖元素截成一张半透明的快照跟着鼠标走。这个交互没毛病但它完全不受 CSS 控制你无法自定义这个幽灵的样式无法让它变成列表占位符、无法分组、无法在拖拽中改变它的内容结构。想在拖拽过程中展示拖到第 2 位之后这种反馈原生 API 做不到你只能自己写。第二个硬伤是触屏支持。HTML5 拖拽在触屏设备上几乎是废的移动端 Safari 直接不支持。就算你用 polyfill也会遇到滚动冲突、点击穿透、触摸延迟这些连锁问题。在一个需要移动端可用的项目里这条路基本走不通。第三个硬伤是数据传递的设计太重。dataTransfer对象要求你把数据序列化成字符串传递对象引用、React 状态、复杂的嵌套结构全都得绕路。而且它的设计是页面级共享多个拖拽源在同一个页面里时你得自己管理 dataTransfer 的命名空间否则很容易串数据。第四个硬伤是动画与状态同步。原生 API 触发的是浏览器层面的拖拽过程和 React 的渲染机制完全脱节。拖拽过程中元素的位移、列表顺序的变化、占位符的插入全都要靠手动操作 DOM 或维护外部状态代码量成倍上涨而且极易出现拖完了视图和数据对不上的问题。1.2 dnd-kit 带来的核心变化dnd-kit 是我当时调研的四个库中最现代的一个。相比 react-dnd 那种集中式全局状态管理的老前辈dnd-kit 把拖拽系统拆成了传感器 拖拽源 放置目标 碰撞检测四层抽象每一层都可以独立替换。这种设计带来的直接好处是边界清晰、扩展容易、性能好控制。更关键的是它对 React 18 的并发特性做了适配。内部的状态更新通过useReducer和useSyncExternalStore管理拖拽过程中的高频事件不会一股脑塞进 React 的批量更新里而是通过对外暴露的监听器做节流。实际体验下来即使在低端 Android 设备上拖着列表快速滑动也很少出现明显丢帧。2. 核心抽象拆解四个你必须理解的积木块dnd-kit 的门槛其实不在 API 本身而在于它的抽象模型。理解了下面这四个概念你写出来的东西才不会是照着文档抄完了却改不动。2.1 Sensors传感器输入源的第一道闸门传感器解决的是用户怎么发出拖拽意图的问题。官方内置了三种PointerSensor鼠标 触摸、KeyboardSensor键盘、TouchSensor专门为触摸优化区别于 PointerSensor 在某些情况下的触摸行为。我重点说PointerSensor的activationConstraint参数这个参数踩坑率极高。它用来设定拖拽激活的触发条件常见的有参数作用典型场景distance移动超过多少像素才激活拖拽防止点击和拖拽混淆比如表格行内的按钮delay按住多少毫秒后才激活需要在拖拽前区分长按和普通点击tolerance允许的误触偏差触屏场景手指轻微抖动不至于直接拖起来那次重构里我遇到过一个极其经典的 bug列表行内有一个可以展开详情的按钮用户点击按钮时偶尔会触发拖拽。排查了半天最后发现是因为我的distance设成了 0意味着鼠标只要移动 1 像素就会激活拖拽。后来我把distance调成了 3问题立刻消失。这个 3px 的容错就是点击和拖拽之间的心理阈值。2.2 useDraggable 与 useDroppable拖拽的两个基本角色useDraggable让一个元素变成可以被拖走的东西useDroppable让一个区域变成可以接收拖拽物的地方。这里面有个初学者最容易误解的点一个元素可以同时是 draggable 和 droppable。这在实现嵌套排序、分组拖拽时非常关键。比如你做一个多层级树形结构父节点本身可以拖动同时它也是子节点的放置目标——这种情况下你在同一个组件里同时调用两个 hook 即可。两个 hook 返回的核心属性有这么几个attributes/listeners需要绑定到拖拽手柄上的属性与事件监听器。注意listeners不要绑到整个卡片上否则会破坏内部按钮和表单控件的点击行为。setNodeRef绑定到 DOM 节点dnd-kit 需要这个 ref 来获取元素尺寸和位置用于碰撞检测。transform一个包含x、y坐标的对象表示当前元素被拖动了多少偏移量。你需要手动把它应用到元素的transform样式上。isDragging当前是否处于拖拽中。用于给被拖元素增加视觉反馈。useDroppable相对简单一些核心是setNodeRef和isOver判断是否有拖拽物悬停在上方。但要注意isOver默认只关心指针是否在区域内如果你需要更精确的判断比如需要指针在区域内且拖动方向朝下才高亮就得配合第三块积木——碰撞检测。2.3 Collision Detection碰撞检测决定拖到哪里的裁判碰撞检测是 dnd-kit 里最值得花时间理解的部分也是它的核心优势所在。内置了几种策略每种策略对当前拖拽物应该落在哪个 droppable 上的判定逻辑完全不同策略原理适用场景rectIntersection计算拖拽元素矩形与每个 droppable 矩形的相交面积取交集最大的那个普通网格、面板拖拽closestCenter计算拖拽元素中心与每个 droppable 中心的距离取最近的列表排序、卡片排列closestCorners计算四个角到 droppable 四个角的距离取最小不规则布局、交叉嵌套区域pointerWithin直接用指针坐标判断落在哪个 droppable 内层级树、文件夹放置我的经验是列表排序用closestCenter因为rectIntersection在元素大小差异明显的列表里容易判定失误。举个例子你拖着一个很大的卡片经过一个小按钮上方矩形相交面积可能更大但用户实际想拖到的位置是卡片下面那个位。中心点距离的判定更符合直觉。当你的 droppable 存在嵌套关系时碰撞检测返回的数组中会同时包含父级和子级。这时不要直接取第一个结果而是需要判断是子级优先还是父级优先。官方提供了getFirstCollision辅助函数但它只取数组第一项——如果你的 droppable 有层级需要根据业务自行过滤。这个细节我在后面嵌套拖拽章节会再讲。2.4 DndContext全局协调者与状态中枢DndContext是整个拖拽系统的大脑它负责收集所有 draggable 和 droppable 的注册信息、调度碰撞检测、维护拖拽状态。所有拖拽逻辑都必须包裹在它内部。它也接收onDragStart、onDragOver、onDragEnd等回调。这里有一个非常重要的设计理念dnd-kit 不帮你管理业务数据的变更它只负责告诉你拖拽发生了什么事具体怎么改你的列表数据完全由你说了算。这跟react-dnd那种后端 管理器的架构很不一样。dnd-kit 用onDragEnd返回的active和over对象分别是拖拽源和放置目标的 id配合你自己的数据自行计算新顺序。这个设计让 dnd-kit 变得非常灵活但也意味着——你必须自己写根据 id 重排数组这种逻辑。onDragEnd回调里拿到的数据中有个非常关键的属性over对象在某些情况下会为null。比如你把元素拖出了所有 droppable 区域over就是 null。你必须在回调里判空否则直接解构over.id会报错。这个错误我见过不少新人在生产环境踩到。3. 从零到一一个可排序列表的完整实现讲完抽象概念我们直接上手。下面的示例是一个常见的纵向可排序列表实现了拖拽排序、拖拽手柄、空状态占位三大功能。代码量不长但每个细节都有讲究。3.1 基础依赖与结构搭建首先是环境版本我在项目中验证过的组合是React 18.2.0dnd-kit/core 6.0.8dnd-kit/sortable 7.0.2dnd-kit/utilities 3.2.2dnd-kit/sortable是在 core 之上封装的专门用于排序场景的扩展包它把重排数组这一步简化了也提供了更稳定的排序动画。直接用 core 也能写排序但需要你自己计算插入位置、处理位移动画工作量会翻倍。3.2 排序容器组件先看最外层容器也就是 DndContext 所在的组件。import { DndContext, closestCenter, PointerSensor, KeyboardSensor, useSensor, useSensors } from dnd-kit/core; import { SortableContext, verticalListSortingStrategy, arrayMove } from dnd-kit/sortable; import { SortableItem } from ./SortableItem; type Item { id: string; content: string }; export function SortableList({ items, onChange }: { items: Item[]; onChange: (items: Item[]) void }) { const sensors useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), useSensor(KeyboardSensor, {}) ); function handleDragEnd(event: any) { const { active, over } event; if (!over) return; if (active.id over.id) return; const oldIndex items.findIndex((item) item.id active.id); const newIndex items.findIndex((item) item.id over.id); const newItems arrayMove(items, oldIndex, newIndex); onChange(newItems); } return ( DndContext sensors{sensors} collisionDetection{closestCenter} onDragEnd{handleDragEnd} SortableContext items{items.map((item) item.id)} strategy{verticalListSortingStrategy} div classNamelist-wrapper {items.map((item) ( SortableItem key{item.id} id{item.id} content{item.content} / ))} /div /SortableContext /DndContext ); }3.3 可排序子项组件关键在于子组件如何把useSortable和CSS.Transform.toString()衔接起来。import { useSortable } from dnd-kit/sortable; import { CSS } from dnd-kit/utilities; export function SortableItem({ id, content }: { id: string; content: string }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } useSortable({ id, }); const style { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.6 : 1, zIndex: isDragging ? 2 : 1, }; return ( div ref{setNodeRef} style{style} classNamesortable-item span classNamedrag-handle {...listeners} {...attributes} ⠿ /span span{content}/span /div ); }这里的CSS.Transform.toString(transform)非常关键。useSortable返回的transform是一个包括x、y、scaleX、scaleY的对象但如果你在拖拽过程中用transition过渡元素会变得黏糊糊的位移不跟手。官方工具函数做了额外处理当 transform 为全零时返回undefined避免掉过渡动画。如果你自己拼字符串一定要做这个空值判断否则会出现拖完了元素还在滑的怪象。3.4 为什么需要 SortableContext如果你只用useDraggable写排序会发现一个问题拖拽一个元素时其余元素不会让位。SortableContext的作用就是维护同一组元素的集体状态当其中一个元素被拖动时它会根据碰撞检测结果计算出其他元素需要偏移多少并且把这些位移通过useSortable的transform返回给每个子组件。这也是排序类拖拽最复杂的部分——让其他元素跟着让路而不是只移动被拖的那个。SortableContext的strategy属性决定偏移方向verticalListSortingStrategy是垂直方向horizontalListSortingStrategy是水平方向rectSortingStrategy是网格方向。注意网格排序不要用前两者否则偏移计算会错乱。4. 真实项目中的进阶玩法拖拽手柄、限制与嵌套Demo 能做出来和能在生产环境稳定运行完全是两码事。这一节我专门整理几个真实场景里几乎必然会用到的进阶配置。4.1 拖拽手柄的设计与可访问性拖拽手柄是 dnd-kit 里最值得讲究的交互细节它直接决定了误操作率。我前面提到listeners绑在整张卡片上会导致按钮点击冲突。最稳妥的做法是把手柄独立成一个小区域只把手柄的listeners和attributes绑上去。手柄区域在移动端还有一个问题触摸滚动时的误触。当你用手指轻轻滑动列表时手指恰好落在手柄上由于PointerSensor的distance参数小于阈值不会激活拖拽因此不会阻断滚动。如果你设了delay还要注意视觉反馈——用户按住了手柄但还没到激活时间此时没有任何反馈会让人困惑。我会在 delay 期间加一个CSS的cursor: grabbing让用户知道已经准备拖了。关于无障碍KeyboardSensor是 dnd-kit 非常加分的一个能力。开启后用户可以用 Tab 聚焦到拖拽手柄然后通过空格或回车激活拖拽再用方向键调整位置。这个能力是原生 HTML5 拖拽完全没有的。在实际项目中启用键盘拖拽并不难只需要把KeyboardSensor加进sensors并且在手柄上绑定attributes和listeners即可。注意KeyboardSensor的coordinateGetter属性默认的 getter 只支持上下左右四个方向如果你的列表是网格布局需要自定义 getter 来允许斜向移动。4.2 限制拖拽范围不只有 DragOverlay很多场景下我们不希望拖拽时整个原始元素跟着指针走而是希望用一个半透明占位来指示当前位置。这需要用到DragOverlay。DragOverlay的作用是渲染一个跟手的浮层而原始元素保持原位或变成占位符。这样有几个好处被拖元素不参与文档流不会因为transform导致布局抖动可以渲染完全不同的视觉内容比如缩小版卡片、带数量角标浮层的 z-index 可控不会跟其他元素纠缠但DragOverlay也带来了额外的复杂度。一个典型的坑是DragOverlay内的内容需要你自行传递选中数据。它并不会自动复制被拖元素你必须在onDragStart时把当前拖拽的数据存到 state 里然后在DragOverlay中渲染。这是最容易困惑的 API——你可能会以为它内部有某种智能克隆机制。另一个坑是当使用DragOverlay时原始元素在拖拽中仍然存在而且useDraggable或useSortable返回的transform仍然有效。如果不做处理原始元素也会跟着移动出现两个元素在飞的 bug。正确做法是当isDragging为 true 时把原始元素的opacity设为 0或者用transform把它置于不可见状态。在dnd-kit/sortable的useSortable中我通常这样处理const style { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0 : 1, // 拖拽中的原始元素不参与布局偏移交给 DragOverlay 表现 };搭配DragOverlay之后视觉上就只有浮层在跟手移动体验提升非常明显。但请注意DragOverlay本身不解决拖拽限制的问题它只是视觉方案。要限制拖拽不超出某个边界你仍然需要手动在onDragMove中计算坐标。4.3 嵌套拖拽树形结构的实现思路在后台系统里最常见的复杂场景就是树形菜单、部门架构、多级分类的排序。dnd-kit 对这种嵌套场景的支持是我选择它而不是 react-dnd 的重要原因之一。嵌套的关键点有三个第一每一层的 droppable 都要能接收跨层拖入的元素。这意味着在onDragEnd中你要判断active和over所在的层级然后执行不同的数据变更逻辑。第二碰撞检测要优先子级。如果被拖元素同时悬停在父节点和子节点上方closestCenter可能返回子节点也可能返回父节点取决于距离。但业务上通常期望落在子节点上优先。这时我建议用pointerWithin作为碰撞策略因为它是严格按照指针坐标来判定的不会因为中心距离的偏差而悬停在错误层级。第三你不能让同一个元素同时出现在多个SortableContext中。如果树形结构中每一层都是一个SortableContext那么处于边界位置的元素可能同时被两个 context 管理导致拖拽时的 transform 计算冲突。我的做法是最外层用一个大SortableContext把所有树形节点的 id 都放进去然后用strategy统一管理。这样虽然少了一些精确控制但稳定性高得多。5. 避坑实录三周重构中我踩过的那些坑这一节写写我在实际重构中遇到的问题。这些问题非常典型几乎每个 dnd-kit 深度用户都会遇到网上 issue 区全都出现过。5.1 拖拽时列表疯狂抖动这是一个标志性问题。症状是拖着一个元素在列表里上下移动其他元素像是跳格子一样疯狂抖动完全没有平滑让路的感觉。排查过程首先怀疑transition。检查后发现useSortable返回的transition已经设置了且style上也挂了。接着怀疑strategy。确认是verticalListSortingStrategy没问题。继续深挖最后发现问题出在容器 CSS 的display: flex和gap属性上。flex gap在 Safari 下的布局计算和 dnd-kit 的预测算法存在冲突导致偏移量计算不准确。解决办法有两个一是给每个sortable-item设置margin-bottom而不是容器gap二是把容器改成display: grid用grid-gap。我选择了后者稳定性和动画效果都更好。这个问题的本质是dnd-kit 的排序算法假设元素之间是相邻排列的gap 会让它的矩形计算产生偏差。如果你不可避免要用 gap一定要在SortableContext外层的 wrapper 上做补偿——具体来说手动给每个 item 的外层包裹一层 div用这个 div 的 padding 作为间隔这样 dnd-kit 计算的矩形不会包含间隔。5.2 点击拖拽手柄却触发了行内按钮这个我前面提到过是listeners绑错了对象。但还有一个更隐蔽的情况即使你把手柄独立了手柄上绑的listeners里包含onPointerDown而这个事件会冒泡到父元素。如果父元素上也有onClick依然会误触。正确做法手柄内部如果有图标或子元素给它们设置pointer-events: none确保所有指针事件统一由手柄容器消费。这个细节在写样式时非常容易遗漏。5.3 onDragEnd 里拿不到最新的数据在onDragEnd回调里读取组件的itemsprop 或 state拿到的可能是旧值。原因是我们写handleDragEnd时利用闭包捕获了items但如果组件因为拖拽过程触发了重渲染而handleDragEnd没有更新闭包就会读取到旧数据。这在 React 18 Concurrent 模式下更容易出现。我的解决方案有两个一是在DndContext的onDragEnd中不直接读 state而是通过useRef保存最新值二是把onDragEnd用useCallback包裹并把items作为依赖项。两者都能解决我更推荐前者因为 useCallback 如果依赖频繁变化会导致 DndContext 反复重渲染性能上不如 ref 稳定。5.4 与 Ant Design 表格的集成问题我们的后台项目大量使用 Ant Design Table。在表格行上实现拖拽排序时遇到一个很不和谐的现象表格的滚动容器会抢走 touch 事件导致在触屏上无法拖拽。原因是 Ant Design Table 的滚动容器监听了touchmove并调用了preventDefault。dnd-kit 的TouchSensor或PointerSensor的触摸分支还没来得及响应事件就被浏览器取消了。解决办法是在DndContext上设置measuring配置或者更直接地给PointerSensor增加delay配置让 dnd-kit 在长按后才激活拖拽从而避开滚动容器的事件拦截。具体 delay 值根据不同设备的滚动灵敏度调整我最终定在 150ms——太短不生效太长让用户觉得拖拽发闷。5.5 拖拽后布局跳动有时候松开鼠标后列表突然弹了一下然后才稳定到排序后的位置。这个现象通常是因为onDragEnd里对数据组的变更导致了 React 重新渲染而 dnd-kit 的动画还没完成新旧布局交替产生了闪跳。解决思路是在onDragEnd里不要立即更新数据而是先调用event.active对应的节点数据计算出新顺序然后通过requestAnimationFrame延迟到下一帧再更新 state。这会让动画先归位再切换数据。代码参考function handleDragEnd(event) { const { active, over } event; if (!over) return; requestAnimationFrame(() { onChange(computeNewOrder(items, active.id, over.id)); }); }如果你想要更丝滑的体验可以考虑使用dnd-kit/core的DragEndEvent返回的delta来手动控制布局动画但这对业务侵入比较大一般不需要。6. 性能优化当列表有几百上千项时我的项目里有个数据看板单列表最多可以渲染 1000 条卡片。最开始直接用 dnd-kit 默认配置拖动起来明显卡顿FPS 掉到 40 以下。经过一轮优化后稳定在 60FPS这里分享一下关键优化点。6.1 禁用不必要的动画拖拽过程中transition样式是其他元素让位时的动画。元素越多同时执行动画的元素越多性能开销越大。当列表超过 200 项时我会动态判断如果items.length 200就把transition设为none。放弃一点动画的平滑度换来的是帧率稳定。6.2 避免在拖拽中重新渲染无关组件dnd-kit 的拖拽状态是通过 React context 传递的。如果一个组件并不关心拖拽状态但它被包裹在DndContext内那么每次拖拽状态更新时它都可能重新渲染。解决办法是拆分组件边界只让需要感知isDragging或isOver的组件订阅 context其余组件用React.memo包裹阻断不必要的更新。另外要注意useSortable返回的transform每次拖拽移动都会更新而transition是不变的。把这两者分开传引用也能减少 React 的 diff 成本。6.3 虚拟滚动与拖拽的冲突处理当列表量级大到必须用虚拟滚动比如react-window时dnd-kit 的默认测量机制会出现问题——虚拟滚动只渲染可视区内的元素dnd-kit 无法测量到所有 droppable 的位置碰撞检测会失准。我的做法是不直接用虚拟滚动包住 droppable 列表而是把可视区外的 droppable 注册为占位节点通过useDroppable的data传入其真实索引位置然后在碰撞检测时用索引判断排列顺序。这兼容了虚拟滚动和拖拽排序但代码复杂度较高需要你对虚拟列表的渲染机制有足够理解。如果非必要建议不做这个优化而是通过分页或分组来降低单屏数量。6.4 使用 Measuring API 调整测量频率dnd-kit 的measuring配置可以控制拖拽过程中是否重新测量 droppable 的位置。默认配置是在拖拽开始时测一次、拖拽过程中按需测量。当列表很长时拖拽过程中的重复测量开销很大因为每次都要获取大量 DOM 节点的getBoundingClientRect。我的配置建议DndContext measuring{{ droppable: { strategy: MeasuringStrategy.WhileDragging, }, }} 如果你确定拖拽过程中容器不会发生尺寸变化可以改成MeasuringStrategy.BeforeDragging这样只在拖拽前测量一次性能提升非常明显代价是如果拖拽中容器尺寸变化碰撞检测会有偏差。7. 和多方案对比为什么不是 react-dnd 或 react-beautiful-dnd当初做技术选型时我分别用 react-dnd、react-beautiful-dnd 和 dnd-kit 写了同一个原型。这里不做什么权威测评只说我的实际感受。维度react-dndreact-beautiful-dnddnd-kit维护状态2024 年已基本停止更新官方已宣布不再维护推荐迁移到 dnd-kit活跃社区持续迭代学习曲线较陡概念多backend/connector平缓API 简单中等核心抽象清晰但需要理解触屏支持需要额外插件支持但依赖 HTML5 draggable原生支持多传感器可选可访问性需要自己实现内置很好内置键盘传感器嵌套拖拽需要手动管理层级不支持原生支持灵活度高性能可接受大列表下滑动能力一般可定制性强优化空间大自定义碰撞检测实现复杂只支持预设支持自定义函数react-beautiful-dnd 的 API 极其友好但它的核心架构决定了它无法很好地支持嵌套拖拽和跨容器拖拽。dnd-kit 在这两点上的设计天然更灵活这也是 Atlassian 官方在 react-beautiful-dnd 停止维护时直接推荐 dnd-kit 的原因之一。当然dnd-kit 也不是没有缺点。它的 API 设计偏向零件化需要你自己组装一些逻辑比如排序数组、碰撞检测结果过滤等。对于只需要一个简单拖拽排序的场景react-beautiful-dnd 可能上手更快。但如果你要长期维护、要做复杂交互dnd-kit 是更稳妥的选择。8. 最后再分享几个实战中的小技巧前面写了很多具体的坑和原理最后补充几个我在实战中沉淀的小技巧它们不会出现在官方文档里但对实际开发很有帮助。第一个是关于调试。拖拽问题的复现和调试很繁琐因为涉及指针坐标、碰撞检测、渲染三者的实时联动。我习惯在开发环境里给DndContext加一个onDragMove把event.delta和当前active.id打印到控制台。这样能快速确认拖拽状态是否更新坐标偏移是否正常。不要过度依赖 React DevTools因为拖拽的高频状态更新会导致调试器卡顿。第二个技巧是给DndContext统一添加autoScroll配置。dnd-kit 默认开启自动滚动但默认的滚动边界和速度不总是符合预期。在长列表中拖拽到边缘时需要容器自动滚动默认的autoScroll有时滚动速度过快或过慢。我通常这样调DndContext autoScroll{{ threshold: { x: 0.2, y: 0.2 }, acceleration: 10, }} threshold表示到达容器边界多少比例时触发滚动acceleration是加速度。这两个值需要按实际布局微调没有万能数值。第三个技巧是关于测试。dnd-kit 的拖拽交互很难用传统单元测试覆盖。我的做法是把数据和排序逻辑比如arrayMove、computeNewOrder独立成纯函数重点测试这些函数拖拽过程中的 DOM 行为用 Playwright 写端到端测试模拟 pointer 事件。dnd-kit官方提供了testing-library/react下的模拟拖拽工具但整体来说拖拽库的测试成本比普通组件高建议把逻辑尽可能从组件中抽离。第四个技巧是版本锁定。dnd-kit 目前仍处于快速迭代期小版本之间可能会有 API 调整。我在 package.json 里锁定精确版本不带^避免团队协作时有人不小心升级后出现不可预期的行为。特别是dnd-kit/core和dnd-kit/sortable的版本必须同步升级否则依赖的core内部类型和sortable的预期不一致编译都过不了。回到最初的问题dnd-kit 到底值不值得用我的答案很明确——如果你的需求不仅仅是把元素拖到某个固定区域而是包含排序、嵌套、跨容器、触屏、无障碍这些真实世界的要求dnd-kit 是当下最优的选择之一。它的学习曲线不陡只要理解了传感器、拖拽源/放置目标、碰撞检测这三个核心抽象写起来很顺手。希望这篇解析能让你少踩几个坑。
返回列表