前端拖拽功能原生实现,不引入拖拽库完成业务
1. 前言拖拽交互在后台管理系统里几乎随处可见比如任务看板、表单排序、图片上传调整顺序等等。很多同学一提到拖拽第一反应就是去装一个 sortablejs 或者 dragula这当然没问题库确实省事。但有时候项目里就一两个拖拽场景为了这点功能引入一个库总觉得有点重。而且有些老项目技术栈比较旧装新依赖还得考虑兼容性折腾半天不如自己动手写。其实浏览器原生就提供了一套完整的拖拽 API也就是 HTML5 的 Drag and Drop。虽然它有一些小坑但应付大多数业务场景完全够用。这篇文章就从一个实际需求出发带大家用原生方式实现一个可拖拽排序的列表全程不依赖任何第三方库。2. 需求分析先说说业务背景。假设我们有一个待办事项列表用户希望可以通过拖拽来调整事项的先后顺序拖完以后顺序要能保存下来下次刷新还是这个顺序。这个需求听起来简单但实现起来要考虑几个点拖拽的时候要给用户一个视觉反馈比如被拖的元素要有点变化目标位置要有个占位提示。拖拽结束以后要能拿到最新的顺序方便提交给后端保存。移动端也要能用不能只在电脑上能拖。明确了需求下面直接看代码。3. 基础 HTML 结构先搭一个最简单的列表结构每个列表项都可以被拖拽。这里用 ul 和 li 来组织每个 li 加上 draggable 属性告诉浏览器这个元素是可以被拖动的。ul idtodoList li draggabletrue>const list document.getElementById(todoList); let draggingItem null; list.addEventListener(dragstart, function (e) { const target e.target.closest(li); if (!target) return; draggingItem target; // 给被拖拽的元素加个样式视觉上区分一下 target.classList.add(dragging); // 记录拖拽的数据这里存一下 id e.dataTransfer.effectAllowed move; e.dataTransfer.setData(text/plain, target.dataset.id); }); list.addEventListener(dragover, function (e) { e.preventDefault(); // 必须阻止默认行为否则 drop 事件不会触发 e.dataTransfer.dropEffect move; const target e.target.closest(li); if (!target || target draggingItem) return; // 判断鼠标在目标元素的上半部分还是下半部分 const rect target.getBoundingClientRect(); const offset e.clientY - rect.top; if (offset rect.height / 2) { list.insertBefore(draggingItem, target); } else { list.insertBefore(draggingItem, target.nextSibling); } }); list.addEventListener(drop, function (e) { e.preventDefault(); }); list.addEventListener(dragend, function (e) { const target e.target.closest(li); if (target) { target.classList.remove(dragging); } draggingItem null; // 拖拽结束输出最新的顺序 const order Array.from(list.children).map(function (li) { return li.dataset.id; }); console.log(最新顺序, order); // 这里可以调用接口把 order 提交给后端保存 });这段代码的核心思路其实很简单在 dragover 的时候根据鼠标位置实时调整被拖元素在列表里的位置这样用户就能看到元素跟着鼠标移动的效果。dragstart 的时候记录一下当前拖的是谁dragend 的时候清理状态并输出结果。5. 样式与视觉反馈光有逻辑还不够得加点样式让用户知道拖拽正在发生。这里给被拖拽的元素加一个半透明效果再给列表加一个拖拽时的背景色变化。#todoList li { padding: 12px 16px; border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: 8px; background: #fff; cursor: grab; list-style: none; transition: transform 0.1s ease; } #todoList li.dragging { opacity: 0.5; border-style: dashed; cursor: grabbing; } #todoList li:hover { border-color: #3b82f6; }这里用了一个小技巧给 li 加了 transition这样拖拽过程中元素移动会有一点点平滑过渡看起来不会那么生硬。当然这个过渡时间不能太长否则拖起来会有延迟感。6. 移动端适配原生 HTML5 拖拽在移动端浏览器上支持得不太好这是个老问题。如果业务确实需要手机端也能拖可以考虑用 touch 事件来模拟。思路是监听 touchstart、touchmove、touchend然后手动计算位置并移动元素。let touchItem null; list.addEventListener(touchstart, function (e) { const target e.target.closest(li); if (!target) return; touchItem target; target.classList.add(dragging); }); list.addEventListener(touchmove, function (e) { e.preventDefault(); if (!touchItem) return; const touch e.touches[0]; const target document.elementFromPoint(touch.clientX, touch.clientY); const li target ? target.closest(li) : null; if (li li ! touchItem) { const rect li.getBoundingClientRect(); if (touch.clientY rect.top rect.height / 2) { list.insertBefore(touchItem, li); } else { list.insertBefore(touchItem, li.nextSibling); } } }); list.addEventListener(touchend, function () { if (touchItem) { touchItem.classList.remove(dragging); touchItem null; } });这段代码的思路和鼠标拖拽基本一致只是把鼠标事件换成了触摸事件。需要注意的是touchmove 里一定要调用 preventDefault否则页面会跟着滚动。7. 总结原生拖拽实现起来并没有想象中那么复杂核心就是 dragstart、dragover、drop 这几个事件的配合。相比引入第三方库原生方案的好处是零依赖、代码可控出问题也好排查。当然它也有缺点比如移动端支持不完善、跨浏览器行为有差异这些都需要在实际项目中权衡。如果只是简单的列表排序原生方案完全够用。如果业务复杂到需要跨列表拖拽、嵌套拖拽、拖拽动画这些高级功能那再考虑引入成熟的拖拽库也不迟。希望这篇文章能给大家提供一个参考下次遇到拖拽需求的时候可以先想想能不能自己写。