draggable="true"仅声明可拖动,需JS监听dragstart/dragover/drop等事件并调用dataTransfer.setData()、preventDefault()及insertBefore()才能实现列表重排;移动端原生不支持,应改用专用库。

draggable="true" 只是起点,不加 JS 无法真正拖动
设置 draggable="true" 只是告诉浏览器“这个元素允许被拖”,它本身不会触发任何 DOM 移动或排序逻辑。如果你只写了 <li draggable="true">买牛奶</li> 而没监听 dragstart、dragover、drop 等事件,拖拽会失败或直接打开新页面(尤其拖图片/链接时)。必须配合 JS 才能实现待办项重排。
常见错误现象:
- 拖动时鼠标变成禁止符号(?)
- 松手后列表没变化,甚至整个页面刷新
- 拖到空白处时提示“此操作不被允许”
- 必须在
dragstart中调用e.dataTransfer.setData(),哪怕只是设个空字符串(如e.dataTransfer.setData('text/plain', '')),否则 Chrome/Firefox 会拒绝后续 drop -
dragover事件默认被阻止,需显式调用e.preventDefault()才能触发drop - 不要给
<ul>或<ol>设draggable="true",它不是容器拖拽,而是列表项拖拽
用 dataTransfer.getData() 和 insertBefore 实现插入位置精准控制
靠 drop 事件的 e.target 很难判断该插到哪个位置——它可能是你拖进去的 <li> 自身,也可能是父 <ul>,还可能是文字节点。正确做法是:在 dragover 阶段就计算光标相对于目标 <li> 的 Y 坐标,标记“插入上方”或“插入下方”,再在 drop 时用 insertBefore() 执行。
示例关键逻辑:
let dropTarget = null;
list.addEventListener('dragover', e => {
e.preventDefault();
const rect = e.target.getBoundingClientRect();
const offset = e.clientY - rect.top;
// 靠近顶部 → 插入到 target 前面;靠近底部 → 插入到 target 后面
dropTarget = offset < rect.height / 2 ? e.target : e.target.nextElementSibling;
});
list.addEventListener('drop', e => {
e.preventDefault();
const dragged = document.querySelector('[data-dragging]');
if (dropTarget && dropTarget !== dragged) {
list.insertBefore(dragged, dropTarget);
}
});-
data-dragging是你在dragstart中临时加的自定义属性(如el.dataset.dragging = 'true'),用于快速定位被拖元素 - 避免用
appendChild(),它只能加到末尾;用insertBefore()才能维持拖拽时的视觉顺序 - 注意
dropTarget可能为null(比如拖到空区域),需兜底处理(例如追加到末尾)
移动端拖拽基本不可用,别指望 touch + draggable 一起工作
原生 draggable + drag* 事件在 iOS Safari 和多数安卓 WebView 中默认禁用或行为异常(比如不触发 dragstart)。这不是代码写错,是浏览器策略限制。试图用 touchstart + touchmove 模拟 drag 事件链,兼容性差且滚动冲突严重。
立即学习“前端免费学习笔记(深入)”;
- 真实项目中,移动端建议改用
sortablejs或react-sortable-hoc这类专为触摸优化的库 - 如果坚持手写,必须监听
touchstart并手动触发dragstart(但部分 iOS 版本仍会静默失败) - 不要在
dragstart里读取event.touches—— 它为空,drag*事件和 touch 事件是两套互不通信的机制
localStorage 持久化拖拽后顺序时,别序列化整个 DOM
很多人拖完直接把 list.innerHTML 存进 localStorage,这会导致:HTML 被转义、事件监听器丢失、自定义属性(如 data-id)混乱、下次加载时所有 draggable="true" 元素失去 JS 绑定。
- 只存结构化数据:每个待办项应有唯一
id和order字段,拖拽后更新数组顺序并存入localStorage - 渲染时用 JS 重建 DOM,而不是
innerHTML = ...,确保addEventListener重新绑定 - 示例存储格式:
JSON.stringify([{id: 'task-1', text: '买牛奶', order: 0}, {id: 'task-2', text: '倒垃圾', order: 1}])
拖拽逻辑本身不复杂,但跨浏览器行为差异、移动端支持缺失、状态持久化方式选择,才是实际落地时最常卡住的地方。



















