一、为什么拖拽如此"上头"?
你有没有注意过,为什么 iPad 一发布就惊艳世界?除了 Retina 屏,流畅的触摸与拖拽体验功不可没。
如今,无论是 Google Drive 的文件上传、Trello 的卡片排序,还是 Figma 的图层移动,拖拽(Drag & Drop) 已成为现代 Web 应用的标配交互。
而这一切,都得益于 HTML5 原生提供的 drag and drop
API。
今天,我们就来手把手实现一个可拖拽图片填充容器的小功能,并深入解析其背后机制,让你在面试和项目中轻松应对。
二、HTML5 拖拽 API
在拖拽系统中,通常有两个角色:
- 拖拽源(Draggable):被拖动的元素
- 投放区(Droppable):可以接收拖拽元素的容器
HTML5 通过几个关键事件实现交互:
事件 | 触发元素 | 说明 |
---|---|---|
dragstart |
拖拽源 | 开始拖动时 |
drag |
拖拽源 | 拖动过程中 |
dragend |
拖拽源 | 拖动结束 |
dragenter |
投放区 | 拖拽进入区域 |
dragover |
投放区 | 拖拽在区域上方移动 |
dragleave |
投放区 | 拖拽离开区域 |
drop |
投放区 | 元素被投放 |
⚠️ 注意:
dragover
事件必须调用preventDefault()
,否则drop
事件不会触发!
三、实现图片拖拽填充
我们来做一个"把图片拖到空白格子中"的小功能。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>图片拖拽 Demo</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="empty">
<div class="fill" draggable="true"></div>
</div>
<div class="empty"></div>
<div class="empty"></div>
<div class="empty"></div>
<div class="empty"></div>
<script src="script.js"></script>
</body>
</html>
3.2 CSS 样式
css
* {
box-sizing: border-box;
}
body {
background-color: steelblue;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
gap: 20px;
padding: 20px;
}
.empty {
height: 150px;
width: 150px;
border: 3px solid #000;
background: white;
transition: all 0.2s ease;
}
.fill {
background-image: url('https://img1.baidu.com/it/u=400864332,910444934&fm=253&fmt=auto&app=138&f=JPEG?w=514&h=500');
width: 145px;
height: 145px;
cursor: grab;
background-position: center;
background-size: cover;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.hold {
border: 5px solid #ccc;
transform: scale(1.05);
}
.hovered {
background-color: #333;
border-color: white;
border-style: dashed;
}
.invisible {
display: none;
}
/* 移动端适配 */
@media (max-width: 768px) {
body {
flex-direction: column;
gap: 10px;
}
.empty, .fill {
width: 120px;
height: 120px;
}
}
3.3 JavaScript 逻辑
javascript
const fill = document.querySelector('.fill');
const empties = document.querySelectorAll('.empty');
// === 拖拽源事件 ===
fill.addEventListener('dragstart', (e) => {
e.target.classList.add('hold');
// 延迟清空,避免拖拽瞬间消失
setTimeout(() => {
e.target.classList.add('invisible');
}, 0);
});
fill.addEventListener('dragend', (e) => {
e.target.classList.remove('hold', 'invisible');
});
// === 投放区事件 ===
empties.forEach(empty => {
empty.addEventListener('dragover', (e) => {
e.preventDefault(); // 关键!允许投放
});
empty.addEventListener('dragenter', (e) => {
e.preventDefault();
empty.classList.add('hovered');
});
empty.addEventListener('dragleave', (e) => {
empty.classList.remove('hovered');
});
empty.addEventListener('drop', (e) => {
e.preventDefault();
empty.classList.remove('hovered');
// 清空当前容器并放入图片
empty.innerHTML = '';
empty.appendChild(fill);
});
});
效果如下:

四、核心机制解析
为什么 dragover
要 preventDefault()
?
浏览器默认会阻止大多数元素的拖放行为(比如防止文本被随意拖走)。
调用 e.preventDefault()
是告诉浏览器:"我已接管此操作,请允许投放"。
✅ 记住口诀:想 drop,先 over,over 必须 preventDefault
draggable="true"
是必须的吗?
- 对于
<img>
、<a>
等元素:默认可拖拽(如拖图片到新标签页) - 对于
<div>
、<p>
等普通元素 :必须显式设置draggable="true"
html
<div draggable="true">我可以被拖动</div>
setTimeout
的妙用
js
setTimeout(() => {
e.target.classList.add('invisible');
}, 0);
如果不加 setTimeout
,拖拽开始瞬间图片就会消失,用户体验差。
加上后,浏览器会先生成"拖拽图像"(drag image),再隐藏原元素,实现"拿起"效果。
五、拖拽上传文件
HTML5 拖拽 API 也支持文件上传!
html
<div id="dropZone" style="height: 200px; border: 2px dashed #ccc; display: flex; align-items: center; justify-content: center;">
拖拽文件到这里上传
</div>
javascript
const dropZone = document.getElementById('dropZone');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.style.backgroundColor = '#f0f0f0';
});
dropZone.addEventListener('dragleave', () => {
dropZone.style.backgroundColor = '';
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.style.backgroundColor = '';
const files = e.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
console.log('上传文件:', file.name, file.size, 'bytes');
// 这里可以上传到服务器
// const formData = new FormData();
// formData.append('file', file);
// fetch('/upload', { method: 'POST', body: formData });
}
});
六、移动端适配问题
虽然 HTML5 拖拽 API 在桌面端很成熟,但在移动端存在兼容性问题:
- iOS Safari 对
dragstart
等事件支持较弱 - 触摸操作与鼠标事件不同
解决方案:
- 使用第三方库 :如 SortableJS、react-dnd
- 监听 touch 事件:手动实现拖拽逻辑
- 降级处理:移动端提供"点击选择"替代方案
七、面试高频问题
Q1:dataTransfer
是什么?怎么用?
e.dataTransfer
是拖拽事件中用于传递数据的对象。
js
// 拖拽源
e.dataTransfer.setData('text/plain', '我是拖拽的数据');
// 投放区
const data = e.dataTransfer.getData('text/plain');
可用于传递文本、URL、文件等。
Q2:如何实现拖拽排序?
思路:记录被拖拽元素的索引,drop
时插入到目标位置。
Q3:拖拽时的"影子图像"能自定义吗?
可以!使用 setDragImage()
:
js
const img = new Image();
img.src = 'custom.png';
e.dataTransfer.setDragImage(img, 10, 10);
八、总结
技术点 | 说明 |
---|---|
draggable="true" |
使元素可拖拽 |
dragstart / dragend |
控制拖拽源样式 |
dragover + preventDefault |
允许投放的关键 |
drop |
最终处理投放逻辑 |
移动端兼容 | 建议使用专业库 |
核心口诀:
- 拖得动:
draggable="true"
- 放得下:
dragover
加preventDefault
- 有反馈:
dragenter
/leave
改样式- 拿得到:
drop
里处理逻辑
参考资料
- MDN: Drag and Drop API
- HTML5 规范文档
- Can I use: Drag and Drop