HTML 拖放 API 怎么用?dragstart 和 dragover 事件详解
结论:HTML5 拖放只有三个必做动作——源元素加 draggable="true" 并在 dragstart 里 setData,目标元素在 dragover 里调用 e.preventDefault(),在 drop 里 getData 处理数据。其中「dragover 忘了 preventDefault」是 drop 事件不触发的头号原因。
dragstart 和 dragover 分别负责什么?
dragstart 由拖动源元素触发,负责「打包数据」;dragover 由放置目标触发,负责「声明这里可以放」。
完整事件顺序是:dragstart → drag(源元素上持续触发)→ dragenter(进入目标)→ dragover(在目标上反复触发)→ drop(松手)→ dragend(源元素上收尾,无论成功与否都会触发)。
关键点:dragover 不是「触发一次」而是持续触发。按 HTML 规范,浏览器对同一目标大约每 350 毫秒(±200 毫秒)触发一次 dragover,鼠标移动时会更快。它既用来控制「能不能放」,也用来实时控制鼠标指针样式。
dragover 为什么必须调用 preventDefault?
结论:dragover 的默认行为是「拒绝放置」,只有调用 e.preventDefault() 才会把当前目标标记为可接受 drop,否则 drop 永远不会触发。
这是拖放调试里最容易踩的坑——dragstart 打了日志、dragenter 也进了,就是没有 drop。原因就是 dragover 里漏了那句 preventDefault()。
另外 dragenter 也建议一起 preventDefault(),否则部分浏览器下目标高亮样式会闪烁。同时在 drop 里也要 preventDefault(),因为浏览器默认会把拖入的链接或图片当作导航跳转处理。
dragstart 里 setData 怎么写?
结论:在 dragstart 里用 e.dataTransfer.setData(format, data) 写入数据,格式名统一用小写(如 text/plain、text/html、application/x-myapp),并同时设置 effectAllowed。
两个实际约束:
第一,Firefox 下如果 dragstart 中没有调用 setData,拖拽根本不会启动。所以哪怕你不需要传数据,也建议写一句 setData('text/plain', '')。
第二,effectAllowed 只能在 dragstart 阶段设置,取值包括 copy、move、link、copyMove、all 等;在其它事件里修改无效。
src.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', src.id);
e.dataTransfer.effectAllowed = 'move';
});
为什么 dragover 里 getData 取不到值?
结论:拖拽过程中数据处于 protected 模式,dragover、dragenter 里调用 getData() 只会返回空字符串,数据必须在 drop 阶段才能读取。
这是规范的安全设计:防止网页在用户还没松手时就窥探拖拽内容。但 e.dataTransfer.types 在 dragover 里是可读的,这正是判断「拖进来的是不是文件」的标准做法:
dst.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = e.dataTransfer.types.includes('Files') ? 'copy' : 'move';
});
dropEffect 默认是 none,必须在 dragover 里显式设置,drop 才会真正被接受,鼠标指针也会相应变化。
完整可运行的最小示例
结论:下面这段代码可以直接复制进 HTML 页面,实现「把 A 拖到 B 里」。
<div id="src" draggable="true">拖我</div>
<div id="dst">放到这里</div>
<script>
const src = document.getElementById('src');
const dst = document.getElementById('dst');
src.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', src.id);
e.dataTransfer.effectAllowed = 'move';
});
dst.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
dst.addEventListener('drop', (e) => {
e.preventDefault();
const id = e.dataTransfer.getData('text/plain');
dst.appendChild(document.getElementById(id));
});
src.addEventListener('dragend', (e) => {
// dropEffect 为 none 表示这次拖拽被取消或落在无效区域
console.log('拖拽结束,结果:', e.dataTransfer.dropEffect);
});
自定义拖拽图像、嵌套区域、移动端有哪些坑?
结论:setDragImage 的图片元素必须真实渲染在文档里且可见,嵌套放置区要用计数器抵消 dragleave,移动端则不要指望原生 HTML5 拖放。
setDragImage(element, x, y) 中的 element 必须是有尺寸的已渲染节点,display: none 的元素不生效;常见做法是用 position: fixed; top: -1000px 把它移出视口而不是隐藏。
嵌套放置区(比如列表里的每个卡片都是 drop target)会出现 dragleave 误触发,因为鼠标从一个子元素移到另一个子元素也会触发 dragleave。解决办法是维护一个 dragenter / dragleave 计数,计数归零才算真正离开。
移动端方面,iOS Safari 直到 15 才提供有限的原生拖放支持,Android Chrome 的表现也不一致。拖拽排序类需求应该在桌面端用 HTML5 拖放、移动端改用 Pointer Events(pointerdown / pointermove / pointerup)手写,或直接用 SortableJS、dnd-kit 这类库统一处理。
最后三个易错点:dataTransfer.setData 的格式字符串统一小写以避免跨浏览器差异;dragover 中 getData 永远为空;<a href>、<img> 和选中文本默认就带 draggable 行为,不需要也不应该再手动加属性。
把这几处对齐,HTML5 拖放基本就没有玄学问题了:源元素负责 dragstart + setData,目标元素负责 dragover + preventDefault,数据在 drop 里取,收尾看 dragend 的 dropEffect。
原文链接:https://www.gj0.com/thread-834.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。