HTML拖放API实战:不依赖库实现列表排序和文件上传

2026-08-17 0 713

前一阵子做一个后台管理系统,需要实现一个常见的功能:拖拽排序。当时第一时间想到的是用 Sortable.js,但项目里根本不想引入额外依赖,而且这个功能就那么一小块,杀鸡用牛刀。于是我就翻了翻 MDN,发现 HTML 原生的拖放 API 完全够用,除了在移动端上体验有点一言难尽,在桌面端它非常靠谱。

这篇文章不写虚的,直接带你从零实现两个功能:拖拽排序列表,以及拖拽文件上传到指定区域。全程原生 HTML + JavaScript,没有任何外部库。

先弄明白拖放 API 的几个核心事件

拖放操作有一堆事件,但新手只需要记住这几个就行:

  • dragstart:当用户开始拖拽元素时触发。
  • dragover:当被拖拽元素悬停在一个可放置区域时触发。
  • dragenterdragleave:进入和离开放置区域时触发,通常用来加个高亮样式。
  • drop:当用户松开鼠标,把元素放到放置区时触发。

还有一个重要的对象:DataTransfer。你可以通过它携带数据,比如被拖拽元素的 id,或者拖拽的文件对象。如果没有这个对象,拖放就像不知道运什么货的货车,毫无意义。

基础:让元素可拖拽

HTML 里并不是所有元素天生就能拖拽的。必须给元素加上 draggable="true" 属性。然后监听 dragstart,在触发时往 dataTransfer 里塞数据。

<div class="list-item" draggable="true" data-id="1">项目一</div>

对应的 dragstart 处理函数:

item.addEventListener('dragstart', function(e) {
    e.dataTransfer.setData('text/plain', this.dataset.id);
    // 设置拖拽时的半透明效果
    this.classList.add('dragging');
});

拖拽结束的时候记得把那个半透明类移除,否则会一直有效。可以用 dragend 事件处理。

实现一个拖拽排序列表

最经典的场景就是列表排序。比如有一个待办事项列表,你可以把某个事项拖到另一个位置。我的做法是这样的:

  1. 每个列表项都可拖拽。
  2. 当某个项被拖动到另一个项的上方时,通过 dragover 计算是否要交换位置。
  3. drop 事件里重新排列 DOM 顺序。

先看核心代码,我把它写成一个简单的 HTML 文件。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>原生拖拽排序</title>
</head>
<body>
    <ul id="sortable-list">
        <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>

    <script>
        const list = document.getElementById('sortable-list');
        let draggingItem = null;

        list.addEventListener('dragstart', function(e) {
            if (e.target.tagName === 'LI') {
                draggingItem = e.target;
                e.dataTransfer.effectAllowed = 'move';
                e.dataTransfer.setData('text/plain', e.target.dataset.id);
                e.target.style.opacity = '0.5';
            }
        });

        list.addEventListener('dragend', function(e) {
            e.target.style.opacity = '';
            draggingItem = null;
        });

        list.addEventListener('dragover', function(e) {
            e.preventDefault(); // 必须调用,否则不允许 drop
            e.dataTransfer.dropEffect = 'move';

            const targetItem = e.target.closest('li');
            if (!targetItem || targetItem === draggingItem) return;

            // 判断鼠标是位于目标的上半部分还是下半部分
            const rect = targetItem.getBoundingClientRect();
            const mouseY = e.clientY - rect.top;
            const before = mouseY  li.dataset.id);
            console.log('新的排序:', sortedIds);
        });
    </script>
</body>
</html>

这段代码的核心就是 dragover 事件里,通过比较鼠标位置与目标元素中心点,来决定是插入到目标的前面还是后面。调用 insertBefore 会实时把拖动中的元素移动位置,这样排序的预览效果非常直观,甚至不用写拖动的幽灵样式。

注意:在 dragover 中必须执行 e.preventDefault(),否则 drop 事件永远不会触发。这是新手最容易忘记的地方。

踩坑:为什么 drop 事件不生效?

我调试的时候发现,只要忘了在 dragover 里加 preventDefault(),你怎么放都没反应。其实这是浏览器默认行为:不允许将元素拖放到另一个元素内部。所以必须把默认行为禁止掉,才能激活 drop

另外一个浪费了我半小时的坑:dragenterdragleave 会疯狂触发,特别是你给放区域加上高亮 class 的时候,鼠标一抖,高亮就会像闪光灯一样闪烁。原因是当你拖动到一个子元素上时,dragleave 会从父元素上触发。解决办法是使用计数器,或者在 dragover 里直接处理样式,而不是依赖 dragenter/leave。我上面的排序代码就没有用 dragenter,而是直接在 dragover 里做判断,简单有效。

更进一步:拖拽文件上传

除了拖拽元素,拖放 API 最常用的场景就是把系统里的文件拖到浏览器窗口里完成上传。这也是 HTML5 时代就有的功能,用起来也不难。我做一个简陋的拖放区域,然后用 FormData 上传到服务器。

先看前端代码:

<div id="drop-zone">把文件拖到这里</div>

<script>
    const dropZone = document.getElementById('drop-zone');

    dropZone.addEventListener('dragover', function(e) {
        e.preventDefault();
        dropZone.style.border = '2px dashed #007bff';
    });

    dropZone.addEventListener('dragleave', function(e) {
        dropZone.style.border = '';
    });

    dropZone.addEventListener('drop', function(e) {
        e.preventDefault();
        const files = e.dataTransfer.files;
        if (!files.length) return;

        // 我们这里假装直接调用上传接口,实际业务中会遍历 files
        uploadFile(files[0]);
        dropZone.style.border = '';
    });

    function uploadFile(file) {
        const formData = new FormData();
        formData.append('file', file);

        fetch('/upload', {
            method: 'POST',
            body: formData
        })
        .then(r => r.json())
        .then(data => console.log('上传成功', data))
        .catch(err => console.error(err));
    }
</script>

就这样,你也可以把多个文件一起处理,只需要遍历 e.dataTransfer.files 数组即可。更骚的操作是,你可以把一张图片拖进页面,然后直接从 dataTransfer.files[0] 里获取文件对象,并且用 URL.createObjectURL(file) 生成预览图,完全不用 input file。

坑:只能在 drop 事件里拿文件

一开始我试图在 dragover 事件里就读取 e.dataTransfer.files,结果发现它是空的。这是因为浏览器出于安全考虑,只有在 drop 时才能真正访问文件列表。所以不要想着在拖拽过程中就预览,只能在松手那一刻去读取。

实现拖拽上传预览

既然说到了预览,我干脆做一个小的功能:把图片拖进去,立即在页面显示缩略图。这里就用到了 URL.createObjectURL,体验非常流畅。

dropZone.addEventListener('drop', function(e) {
    e.preventDefault();

    const files = e.dataTransfer.files;
    for (const file of files) {
        if (file.type.startsWith('image/')) {
            const img = document.createElement('img');
            img.src = URL.createObjectURL(file);
            img.width = 120;
            img.style.margin = '5px';
            document.body.appendChild(img);
        }
    }
});

不过这里有个内存泄漏的问题:用过的 objectURL 需要手动释放。当你不再需要图片时,调用 URL.revokeObjectURL(url) 即可。在单页应用里,不注意这个会越用越卡。

更优雅的实现:使用 dataTransfer 的 setDragImage

默认情况下,拖动元素时,浏览器会生成一个半透明的元素副本跟着鼠标走。但你想要自定义拖拽时的“幽灵”图片,可以用 setDragImage。这个 API 接受三个参数:自定义元素、X偏移量、Y偏移量。

dragItem.addEventListener('dragstart', function(e) {
    const ghost = document.createElement('div');
    ghost.textContent = '正在移动...';
    ghost.style.position = 'absolute';
    ghost.style.top = '-9999px';
    document.body.appendChild(ghost);
    e.dataTransfer.setDragImage(ghost, 0, 0);
});

然后需要在下一次 tick 时移除这个元素。我一般用 setTimeout 或者在 dragend 里手动 remove。不过默认的拖拽效果其实已经够用了,我很少弄这个。

最终美化一下排序列表的交互

原生拖放真的特别像“素颜”,朴实但能用。为了提高体验,我习惯在 dragover 时给被拖拽的项加一个类,表示它正在被准备放下。比如在列表项下面画一条横线?这就得用 CSS,但为了不违背你这篇不写样式的要求,我就不展开实际代码。不过我可以告诉你思路:给 li 绑定 dragover,然后动态在它前/后插入一个空白占位元素,排序效果会非常平滑。

但我上面的排序代码已经可以直接用了,因为 insertBefore 会实时改变 DOM 位置,视觉上感觉像在“穿插”,其实也不差。

移动端:原生拖放的致命弱点

让我必须说明白:原生拖放 API 在移动端浏览器上基本不可用。所以如果你的项目需要支持手机触屏拖拽,还是老老实实引入像 vuedraggable 或 Sortable.js 这样的库。但是桌面端后台管理系统,用原生拖放完全足够。

另外,如果你要拖放的对象有滚动容器,注意在 dragover 时可能需要通过 element.scrollTop += ... 来自动滚动,不然拖到边缘时列表不会滚动到更远的位置。

一点小总结

HTML 原生拖放 API 看起来有点古老,但它依然是许多桌面端交互的基石。我用它给项目写过一个配置项拖拽排序,还写过一个批量文件上传区,都挺稳定。关键是代码量少,不需要额外学习库的接口。你只要记住了 dragoverpreventDefaultdrop 里才拿数据,基本就不会遇到太大障碍。

如果你正在纠结要不要为一个小功能引入拖拽库,不妨先试试原生方案。至少对我来说,它帮我避免了一次依赖体积膨胀。

HTML拖放API实战:不依赖库实现列表排序和文件上传
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 html HTML拖放API实战:不依赖库实现列表排序和文件上传 https://www.taomawang.com/web/html/2561.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务