一、为什么还要自己写拖拽
做前端的朋友应该都有体会,拖拽这个交互看着简单,真要做起来却有不少门道。市面上像 SortableJS、dragula 这类库确实成熟,拿来就能用,但很多时候我们只是需要一个轻量的排序或者移动功能,为了这点需求引入一个库,反而让打包体积变大,还要去读它的文档和 API。
其实浏览器原生就提供了一套完整的拖拽能力,也就是 HTML5 的 Drag and Drop 接口。它支持我们自定义拖拽的样式、数据传递和放置逻辑,完全够用。这篇文章就从一个实际业务场景出发,带大家用原生方式实现一个可拖拽排序的列表,全程不依赖任何第三方库。
二、先理清原生拖拽的几个核心概念
在写代码之前,有几个概念得先弄清楚,不然容易绕晕。
- 拖拽源:被拖动的元素,也就是我们列表里的每一项。
- 放置目标:允许接收被拖元素的位置,通常是列表容器本身。
- 数据传输:拖拽过程中,我们需要把被拖元素的标识信息存起来,方便在放置时读取。
原生拖拽主要依赖下面这几个事件:dragstart、dragover、drop 和 dragend。其中 dragstart 在开始拖拽时触发,dragover 在拖拽经过某个元素时持续触发,drop 在松手放置时触发,dragend 则在整个拖拽流程结束时触发。
三、动手实现一个可拖拽排序列表
下面我们直接进入正题,用原生 JavaScript 写一个简单的任务列表,支持拖拽调整顺序。先看 HTML 结构:
html
<ul id="taskList">
<li draggable="true" data-id="1">完成需求评审</li>
<li draggable="true" data-id="2">编写接口文档</li>
<li draggable="true" data-id="3">开发前端页面</li>
<li draggable="true" data-id="4">联调与自测</li>
</ul>
这里的关键是给每个 li 加上 draggable="true",同时用 data-id 记录每一项的唯一标识。接下来是核心的 JavaScript 逻辑:
javascript
const list = document.getElementById('taskList');
let draggedItem = null;
list.addEventListener('dragstart', function (e) {
const target = e.target.closest('li');
if (!target) return;
draggedItem = target;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', target.dataset.id);
target.classList.add('dragging');
});
list.addEventListener('dragover', function (e) {
e.preventDefault();
const target = e.target.closest('li');
if (!target || target === draggedItem) return;
const rect = target.getBoundingClientRect();
const after = (e.clientY - rect.top) > rect.height / 2;
if (after) {
target.after(draggedItem);
} else {
target.before(draggedItem);
}
});
list.addEventListener('drop', function (e) {
e.preventDefault();
});
list.addEventListener('dragend', function () {
if (draggedItem) {
draggedItem.classList.remove('dragging');
draggedItem = null;
}
// 这里可以读取列表顺序,提交给后端保存
const order = [...list.querySelectorAll('li')].map(li => li.dataset.id);
console.log('最新顺序:', order);
});
这段代码的思路其实很直白:拖拽开始时记录被拖元素,拖拽经过其他项时,根据鼠标位置判断是插到目标前面还是后面,松手后清理样式并输出最新顺序。整个过程没有引入任何库,逻辑也完全可控。
四、几个容易踩坑的细节
原生拖拽虽然方便,但有几个细节不注意就会出问题。
第一,dragover 事件里必须调用 e.preventDefault(),否则 drop 事件根本不会触发,这是很多新手容易忽略的地方。第二,dataTransfer.setData 在拖拽过程中用来传递数据,虽然我们这里直接用变量记录了元素,但在跨区域拖拽时,这个 API 是必不可少的。第三,拖拽过程中元素会有一个默认的半透明效果,我们可以通过 CSS 自定义,让交互反馈更明显。
css
li.dragging {
opacity: 0.5;
border: 1px dashed #409eff;
}
五、从排序到更复杂的业务场景
掌握了基础排序之后,稍微扩展一下就能应对更多业务。比如把列表换成表格的行,或者把拖拽源和放置目标分开,实现跨列移动。再比如,我们可以在 drop 事件里读取 dataTransfer 中的数据,配合后端接口完成持久化。
原生拖拽的另一个优势是性能。对于几十个节点的列表,直接操作 DOM 的排序方式非常轻快,不会有库带来的额外渲染开销。当然,如果你的业务涉及跨浏览器兼容、触屏设备或者非常复杂的嵌套结构,那还是建议评估一下成熟的拖拽库,毕竟它们处理边界情况更完善。
六、写在最后
这篇文章从一个实际需求出发,带大家用原生 HTML5 拖拽 API 实现了一个可排序列表。核心就是理解 dragstart、dragover、drop 这几个事件的分工,再配合一点 DOM 操作就能完成。希望下次遇到类似需求时,你能先想想原生方案,而不是急着引入一个库。