拖拽功能需严格遵循三步闭环:dragstart中必须调用setdata()传结构化数据,dragover必须preventdefault()并设dropeffect,drop时应更新状态数组而非直接操作dom。

dragstart里不调setData(),拖拽就静默失效
Chrome 和 Safari 对 draggable="true" 的普通元素(比如 <div>)有严格限制:没在 <code>dragstart 里调 dataTransfer.setData(),哪怕只传空字符串,整个拖拽流程就会被浏览器直接忽略——控制台零报错、没日志、看起来“能拖但松手回弹”。
- 必须用
e.dataTransfer.setData("application/json", JSON.stringify(config)),别用"text/plain"传中文或嵌套对象,Safari 容易截断 -
setData()第二个参数只能是字符串,传{type: "button"}会变成"[object Object]",后续无法解析 - 每个可拖项建议带
data-component-type属性,避免 JS 里写死if (id === "btn")判断类型
dragover不preventDefault(),drop事件根本不会触发
这是最多人卡住的点:只在 drop 里写了 preventDefault(),但 dragover 没拦,默认行为被浏览器执行,导致投放区“拒收”所有拖拽——drop 事件压根不会触发。
-
dragover处理函数第一行必须是e.preventDefault() - 建议同步设
e.dataTransfer.dropEffect = "move",否则光标可能显示为禁止符号 - 如果投放区是滚动容器(如侧边栏面板),监听器要挂载在最外层根节点,用事件委托;绑在内部子项上,滚动后子项重绘会导致监听丢失
- 避免在
dragover里频繁调getBoundingClientRect()或改样式,容易卡顿;可用requestAnimationFrame节流
drop里直接appendChild(),后续状态就失控
真实项目里,拖进画布的不该是 DOM 节点本身,而是描述它的配置对象。直接 appendChild() 看似快,但一旦涉及撤销、持久化、跨 iframe 同步或服务端保存,立刻崩。
- 统一维护一个数组(如
layout)存所有组件的{ type, props, id } -
drop时只往数组里push()或splice(),再由渲染函数批量生成 DOM - 不要用
innerHTML重写整个容器,会清空<input>当前值和绑定事件 - 若需支持排序(如表格行),用
targetTr.before(draggedTr)或targetTr.after(draggedTr),别依赖坐标算索引
移动端和 Safari 的兼容性坑
原生拖拽 API 在移动端默认不可用,Safari 对 MIME 类型也更敏感,不处理就会白屏或静默失败。
- 移动端得靠
touchstart/touchmove/touchend模拟拖拽逻辑,不能只依赖 drag API - Safari 不支持
"application/json"类型的getData()返回值为空字符串,得 fallback 到"text/plain"并手动校验 - 禁用
user-select: none防止拖拽时意外选中文本,尤其在可编辑区域附近 - 别用
window.innerWidth做限位计算,它含滚动条宽度;优先用document.documentElement.clientWidth
draggable="true" 就完事,真正麻烦的是三件事闭环:源端传对结构化数据、目标端主动开门许可、DOM 更新与状态彻底解耦——漏掉任一环,问题都藏在看不见的地方。











