必须在dragstart中调用e.dataTransfer.setData(),否则Chrome/Firefox会静默拒绝拖拽,drop事件根本不触发;dragover必须每次调用preventDefault()才能启用drop;换位需用insertBefore而非appendChild。

dragstart里不设dataTransfer就根本拖不动
Chrome 和 Firefox 会直接拒绝后续的 drop,哪怕你写了 draggable="true"。这不是浏览器 bug,是规范强制要求:必须在 dragstart 中调用 e.dataTransfer.setData(),哪怕只是塞个空字符串。
常见错误现象:drop 事件完全不触发,或者松手后元素“消失”——其实是被浏览器当成链接或图片打开了新页面。
-
e.dataTransfer.setData('text/plain', e.target.id)是最稳妥的选择,兼容性好、语义清晰 - 别用
"Text"或"text"当 key,Firefox 会误判为导航意图,打开新标签页 - 如果拖多个项,建议统一加
e.dataTransfer.effectAllowed = 'move',让光标显示“移动”样式
dragover 不 preventDefault 就永远等不到 drop
dragover 事件默认被浏览器阻止,这是为了防止随意投放。不显式调用 e.preventDefault(),drop 就不会触发——不是延迟,是压根不发。
容易踩的坑:dragover 绑定在父容器(比如 <ul>)上,但事件实际冒泡到文字节点或子元素,导致 e.target 不是你预期的 <li>。
立即学习“前端免费学习笔记(深入)”;
- 监听时用事件委托,绑定在父容器,再判断
e.target.nodeName === 'LI' - 每次
dragover都要e.preventDefault(),不能只在第一次调用 - 不要在
dragenter里preventDefault就以为够了——dragover才是关键闸门
drop 里靠 insertBefore 实现精准换位,不是 appendChild
appendChild() 只能把元素塞到末尾,而拖拽换位需要维持视觉顺序:A 拖到 B 中间,就得插在 B 前面或后面。insertBefore() 才是唯一能控制插入点的 DOM 方法。
难点不在调用函数,而在确定“插在哪”:松手瞬间的 e.target 可能是自身、父节点、文本节点,甚至 <span> 子元素。
- 推荐在
dragover阶段就计算光标 Y 坐标与目标元素高度比,标记插入位置(如dropTarget = offset ) - 用临时属性标记被拖元素,比如
el.dataset.dragging = 'true',避免在drop里反复查 DOM - 插入前务必检查
dropTarget !== dragged,否则可能把元素插回原位,造成视觉卡顿
移动端原生 draggable 基本不可靠
iOS Safari 和部分 Android WebView 对 draggable="true" 支持极差:拖动卡顿、dragstart 不触发、drop 无响应。这不是代码写错了,是 WebKit 引擎长期未完善该 API。
如果你的目标用户包含大量移动端,别试图“修”原生行为——直接换方案。
- 改用
touchstart/touchmove/touchend+ CSS transform 模拟拖拽,配合pointer-events: none临时禁用被拖元素交互 - 引入轻量库如 SortableJS,它已封装好移动端适配和动画
- 服务端渲染场景下,可先禁用移动端拖拽,改用“上移/下移”按钮操作
真正难的不是写出能跑通的 demo,而是处理 dragover 中坐标计算的边界情况、跨容器拖拽时的 parent 切换、以及 iOS 上 touch 事件与 drag API 的互斥——这些细节不暴露在初始测试里,只在真实用户滚动、缩放、快速拖拽时浮现。



















