一、基础概念
HTML5 Drag & Drop API 是浏览器原生提供的拖拽功能,允许用户通过鼠标拖动元素并将其放置到目标位置。
核心角色
- 拖拽源(Drag Source):可以被拖动的元素
- 拖放目标(Drop Target):可以接收被拖动元素的区域
二、完整的拖拽流程
2.1 事件流程图
用户按下鼠标并开始拖动
↓
dragstart 事件 (在拖拽源触发,只触发一次)
↓
drag 事件 (在拖拽源持续触发,拖动过程中)
↓
dragenter 事件 (鼠标进入拖放目标时触发)
↓
dragover 事件 (鼠标在拖放目标上方时持续触发)
↓
dragleave 事件 (鼠标离开拖放目标时触发)
↓
drop 事件 (在拖放目标释放鼠标时触发)
↓
dragend 事件 (在拖拽源触发,拖拽结束)2.2 事件详解
| 事件名称 | 触发位置 | 触发时机 | 触发频率 |
|---|---|---|---|
dragstart | 拖拽源 | 开始拖动时 | 一次 |
drag | 拖拽源 | 拖动过程中 | 持续触发 |
dragenter | 拖放目标 | 进入目标区域 | 一次 |
dragover | 拖放目标 | 在目标区域上方 | 持续触发 |
dragleave | 拖放目标 | 离开目标区域 | 一次 |
drop | 拖放目标 | 释放鼠标 | 一次 |
dragend | 拖拽源 | 拖拽结束 | 一次 |
三、基础用法示例
3.1 最简单的拖拽实现
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>HTML5 拖拽示例</title>
<style>
.drag-source {
width: 200px;
padding: 20px;
background: #4CAF50;
color: white;
cursor: move;
margin: 20px;
}
.drop-target {
width: 300px;
height: 200px;
border: 2px dashed #999;
padding: 20px;
margin: 20px;
}
.drop-target.drag-over {
background: #e3f2fd;
border-color: #2196F3;
}
</style>
</head>
<body>
<!-- 拖拽源 -->
<div class="drag-source" draggable="true" id="dragItem">
拖动我
</div>
<!-- 拖放目标 -->
<div class="drop-target" id="dropZone">
拖放到这里
</div>
<script>
const dragItem = document.getElementById('dragItem');
const dropZone = document.getElementById('dropZone');
// 1. 拖拽开始
dragItem.addEventListener('dragstart', function(e) {
console.log('dragstart: 开始拖拽');
// 设置拖拽数据
e.dataTransfer.setData('text/plain', '这是拖拽的数据');
// 设置拖拽效果
e.dataTransfer.effectAllowed = 'copy';
});
// 2. 拖拽结束
dragItem.addEventListener('dragend', function(e) {
console.log('dragend: 拖拽结束');
});
// 3. 进入目标区域
dropZone.addEventListener('dragenter', function(e) {
console.log('dragenter: 进入目标区域');
e.preventDefault();
this.classList.add('drag-over');
});
// 4. 在目标区域上方(必须阻止默认行为)
dropZone.addEventListener('dragover', function(e) {
e.preventDefault(); // 必须调用,否则 drop 事件不会触发
e.dataTransfer.dropEffect = 'copy';
});
// 5. 离开目标区域
dropZone.addEventListener('dragleave', function(e) {
console.log('dragleave: 离开目标区域');
this.classList.remove('drag-over');
});
// 6. 放置到目标区域
dropZone.addEventListener('drop', function(e) {
e.preventDefault(); // 阻止默认行为(如打开链接)
this.classList.remove('drag-over');
// 获取拖拽数据
const data = e.dataTransfer.getData('text/plain');
console.log('drop: 接收到数据:', data);
// 显示数据
this.innerHTML = `<p>接收到: ${data}</p>`;
});
</script>
</body>
</html>3.2 关键点说明
- draggable="true":使元素可拖拽
- e.preventDefault():在
dragover和drop中必须调用,否则拖放无效 - dataTransfer:用于在拖拽源和拖放目标之间传递数据
四、DataTransfer 对象详解
4.1 核心属性
| 属性 | 说明 | 设置位置 |
|---|---|---|
effectAllowed | 允许的拖放效果 | 拖拽源(dragstart) |
dropEffect | 实际的拖放效果 | 拖放目标(dragover) |
files | 拖拽的文件列表 | 只读 |
types | 数据类型列表 | 只读 |
items | 数据项列表 | 只读 |
4.2 核心方法
// 设置数据
e.dataTransfer.setData(format, data);
// 获取数据
const data = e.dataTransfer.getData(format);
// 清除数据
e.dataTransfer.clearData(format);
// 设置拖拽图像
e.dataTransfer.setDragImage(element, xOffset, yOffset);五、鼠标效果控制
5.1 effectAllowed(拖拽源设置)
在 dragstart 事件中设置,定义允许的操作类型:
dragItem.addEventListener('dragstart', function(e) {
// 可选值
e.dataTransfer.effectAllowed = 'none'; // 不允许
e.dataTransfer.effectAllowed = 'copy'; // 复制
e.dataTransfer.effectAllowed = 'move'; // 移动
e.dataTransfer.effectAllowed = 'link'; // 链接
e.dataTransfer.effectAllowed = 'copyMove'; // 复制或移动
e.dataTransfer.effectAllowed = 'copyLink'; // 复制或链接
e.dataTransfer.effectAllowed = 'linkMove'; // 链接或移动
e.dataTransfer.effectAllowed = 'all'; // 所有操作
});5.2 dropEffect(拖放目标设置)
在 dragover 事件中设置,定义实际执行的操作:
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
// 可选值
e.dataTransfer.dropEffect = 'none'; // 禁止放置 🚫
e.dataTransfer.dropEffect = 'copy'; // 复制 ➕
e.dataTransfer.dropEffect = 'move'; // 移动
e.dataTransfer.dropEffect = 'link'; // 链接 🔗
});六、数据传递
6.1 基本数据类型传递
// 拖拽源:设置数据
dragItem.addEventListener('dragstart', function(e) {
// 文本数据
e.dataTransfer.setData('text/plain', 'Hello World');
// HTML 数据
e.dataTransfer.setData('text/html', '<strong>Bold Text</strong>');
// URL 数据
e.dataTransfer.setData('text/uri-list', 'https://example.com');
});
// 拖放目标:获取数据
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
// 获取文本数据
const text = e.dataTransfer.getData('text/plain');
console.log('接收到文本:', text);
// 获取 HTML 数据
const html = e.dataTransfer.getData('text/html');
console.log('接收到 HTML:', html);
});6.2 传递对象数据(JSON 序列化)
由于 dataTransfer 只能传递字符串,传递对象需要序列化:
<!DOCTYPE html>
<html>
<head>
<style>
.item {
padding: 15px;
margin: 10px;
background: #2196F3;
color: white;
cursor: move;
display: inline-block;
}
.drop-zone {
min-height: 150px;
border: 2px dashed #999;
padding: 20px;
margin: 10px;
}
</style>
</head>
<body>
<div class="item" draggable="true" data-id="1" data-name="产品A" data-price="99.99">
产品A - ¥99.99
</div>
<div class="item" draggable="true" data-id="2" data-name="产品B" data-price="199.99">
产品B - ¥199.99
</div>
<div class="drop-zone" id="cart">购物车(拖放产品到这里)</div>
<script>
// 拖拽源:序列化对象
document.querySelectorAll('.item').forEach(item => {
item.addEventListener('dragstart', function(e) {
// 创建对象
const product = {
id: this.dataset.id,
name: this.dataset.name,
price: parseFloat(this.dataset.price)
};
// 序列化为 JSON 字符串
const jsonData = JSON.stringify(product);
e.dataTransfer.setData('application/json', jsonData);
console.log('拖拽产品:', product);
});
});
// 拖放目标:反序列化对象
const cart = document.getElementById('cart');
cart.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
cart.addEventListener('drop', function(e) {
e.preventDefault();
// 获取 JSON 字符串
const jsonData = e.dataTransfer.getData('application/json');
// 反序列化为对象
const product = JSON.parse(jsonData);
console.log('接收到产品:', product);
// 显示产品信息
const productDiv = document.createElement('div');
productDiv.style.cssText = 'padding: 10px; margin: 5px; background: #4CAF50; color: white;';
productDiv.innerHTML = `
<strong>${product.name}</strong><br>
ID: ${product.id}<br>
价格: ¥${product.price}
`;
this.appendChild(productDiv);
});
</script>
</body>
</html>6.3 传递复杂对象的注意事项
// ❌ 错误:直接传递对象(会被转换为 "[object Object]")
e.dataTransfer.setData('text/plain', { name: 'John' });
// ✅ 正确:序列化后传递
const data = { name: 'John', age: 30, tags: ['developer', 'designer'] };
e.dataTransfer.setData('application/json', JSON.stringify(data));
// 接收时反序列化
const receivedData = JSON.parse(e.dataTransfer.getData('application/json'));
console.log(receivedData.name); // 'John'
console.log(receivedData.tags); // ['developer', 'designer']6.4 传递多种格式的数据
dragItem.addEventListener('dragstart', function(e) {
const data = {
id: 123,
title: '示例标题',
content: '示例内容'
};
// 同时设置多种格式
e.dataTransfer.setData('text/plain', data.title);
e.dataTransfer.setData('text/html', `<h1>${data.title}</h1><p>${data.content}</p>`);
e.dataTransfer.setData('application/json', JSON.stringify(data));
});
// 接收时根据需要选择格式
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
// 检查可用的数据类型
console.log('可用类型:', e.dataTransfer.types);
// 优先使用 JSON 格式
if (e.dataTransfer.types.includes('application/json')) {
const data = JSON.parse(e.dataTransfer.getData('application/json'));
console.log('使用 JSON 数据:', data);
} else if (e.dataTransfer.types.includes('text/html')) {
const html = e.dataTransfer.getData('text/html');
console.log('使用 HTML 数据:', html);
} else {
const text = e.dataTransfer.getData('text/plain');
console.log('使用纯文本数据:', text);
}
});七、跨窗口拖拽
7.1 同源窗口间拖拽
HTML5 Drag & Drop API 支持在同源的不同窗口/标签页之间拖拽:
<!-- 窗口 A: source.html -->
<!DOCTYPE html>
<html>
<head>
<title>拖拽源窗口</title>
<style>
.drag-item {
padding: 20px;
background: #4CAF50;
color: white;
cursor: move;
margin: 20px;
display: inline-block;
}
</style>
</head>
<body>
<h2>拖拽源窗口</h2>
<p>将下面的元素拖到另一个窗口</p>
<div class="drag-item" draggable="true" id="item">
拖动我到另一个窗口
</div>
<script>
document.getElementById('item').addEventListener('dragstart', function(e) {
const data = {
message: '来自窗口 A 的数据',
timestamp: new Date().toISOString(),
windowName: 'Window A'
};
e.dataTransfer.setData('application/json', JSON.stringify(data));
e.dataTransfer.effectAllowed = 'copy';
console.log('开始跨窗口拖拽:', data);
});
</script>
</body>
</html><!-- 窗口 B: target.html -->
<!DOCTYPE html>
<html>
<head>
<title>拖放目标窗口</title>
<style>
.drop-zone {
min-height: 200px;
border: 3px dashed #999;
padding: 20px;
margin: 20px;
text-align: center;
}
.drop-zone.drag-over {
background: #e3f2fd;
border-color: #2196F3;
}
.received-item {
padding: 15px;
margin: 10px;
background: #4CAF50;
color: white;
border-radius: 4px;
}
</style>
</head>
<body>
<h2>拖放目标窗口</h2>
<p>从另一个窗口拖拽元素到这里</p>
<div class="drop-zone" id="dropZone">
<p>拖放区域</p>
<p style="color: #999;">从另一个窗口拖拽元素到这里</p>
</div>
<script>
const dropZone = document.getElementById('dropZone');
dropZone.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
dropZone.addEventListener('dragleave', function(e) {
this.classList.remove('drag-over');
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
// 接收跨窗口数据
const jsonData = e.dataTransfer.getData('application/json');
if (jsonData) {
const data = JSON.parse(jsonData);
console.log('接收到跨窗口数据:', data);
// 显示接收到的数据
const itemDiv = document.createElement('div');
itemDiv.className = 'received-item';
itemDiv.innerHTML = `
<strong>接收到数据:</strong><br>
消息: ${data.message}<br>
来源: ${data.windowName}<br>
时间: ${data.timestamp}
`;
this.appendChild(itemDiv);
}
});
</script>
</body>
</html>7.2 跨窗口拖拽的限制
- 同源策略:只能在同源(相同协议、域名、端口)的窗口间拖拽
- 数据类型限制:只能传递字符串数据,复杂对象需要序列化
- 安全限制:某些浏览器可能限制跨窗口拖拽功能
7.3 跨窗口拖拽文件
<!DOCTYPE html>
<html>
<head>
<title>文件拖放</title>
<style>
.file-drop-zone {
min-height: 200px;
border: 3px dashed #999;
padding: 40px;
text-align: center;
margin: 20px;
}
.file-drop-zone.drag-over {
background: #e8f5e9;
border-color: #4CAF50;
}
.file-list {
margin-top: 20px;
text-align: left;
}
.file-item {
padding: 10px;
margin: 5px 0;
background: #f5f5f5;
border-left: 4px solid #4CAF50;
}
</style>
</head>
<body>
<h2>文件拖放示例</h2>
<p>从文件管理器拖拽文件到下方区域</p>
<div class="file-drop-zone" id="fileDropZone">
<p style="font-size: 48px;">📁</p>
<p>拖拽文件到这里</p>
<p style="color: #999; font-size: 14px;">支持从文件管理器或其他窗口拖拽</p>
</div>
<div class="file-list" id="fileList"></div>
<script>
const fileDropZone = document.getElementById('fileDropZone');
const fileList = document.getElementById('fileList');
fileDropZone.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
fileDropZone.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
fileDropZone.addEventListener('dragleave', function(e) {
this.classList.remove('drag-over');
});
fileDropZone.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
// 获取拖拽的文件
const files = e.dataTransfer.files;
console.log('接收到文件数量:', files.length);
// 显示文件信息
Array.from(files).forEach(file => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.innerHTML = `
<strong>📄 ${file.name}</strong><br>
类型: ${file.type || '未知'}<br>
大小: ${(file.size / 1024).toFixed(2)} KB<br>
最后修改: ${new Date(file.lastModified).toLocaleString()}
`;
fileList.appendChild(fileItem);
});
});
</script>
</body>
</html>7.4 检测拖拽来源
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
// 检查是否是文件
if (e.dataTransfer.files.length > 0) {
console.log('拖拽来源: 文件系统');
console.log('文件数量:', e.dataTransfer.files.length);
}
// 检查是否有 URL
else if (e.dataTransfer.types.includes('text/uri-list')) {
const url = e.dataTransfer.getData('text/uri-list');
console.log('拖拽来源: URL -', url);
}
// 检查是否有自定义数据
else if (e.dataTransfer.types.includes('application/json')) {
const data = JSON.parse(e.dataTransfer.getData('application/json'));
console.log('拖拽来源: 自定义数据 -', data);
}
// 纯文本
else if (e.dataTransfer.types.includes('text/plain')) {
const text = e.dataTransfer.getData('text/plain');
console.log('拖拽来源: 纯文本 -', text);
}
});7.5 常用数据类型说明
HTML5 Drag & Drop API 支持多种标准 MIME 类型:
| 数据类型 | 说明 | 使用场景 | 浏览器支持 |
|---|---|---|---|
text/plain | 纯文本 | 拖拽文本内容 | ✅ 所有浏览器 |
text/html | HTML 内容 | 拖拽富文本 | ✅ 所有浏览器 |
text/uri-list | URI 列表 | 拖拽链接、书签 | ✅ 所有浏览器 |
application/json | JSON 数据 | 自定义数据传递 | ✅ 所有浏览器 |
Files | 文件对象 | 拖拽文件 | ✅ 所有浏览器 |
text/uri-list 详解
text/uri-list 是一个标准的 MIME 类型,用于传递一个或多个 URI(统一资源标识符)。
格式规范:
- 每个 URI 占一行
- 以
#开头的行是注释 - 空行会被忽略
- 多个 URI 用换行符
\n分隔
使用示例:
<!DOCTYPE html>
<html>
<head>
<style>
.link-item {
padding: 15px;
margin: 10px;
background: #2196F3;
color: white;
cursor: move;
display: inline-block;
text-decoration: none;
border-radius: 4px;
}
.drop-zone {
min-height: 200px;
border: 2px dashed #999;
padding: 20px;
margin: 20px;
}
.received-link {
padding: 10px;
margin: 5px 0;
background: #f5f5f5;
border-left: 4px solid #4CAF50;
}
</style>
</head>
<body>
<h2>拖拽链接示例</h2>
<!-- 可拖拽的链接 -->
<a href="https://www.example.com" class="link-item" draggable="true" id="link1">
拖动这个链接
</a>
<a href="https://www.github.com" class="link-item" draggable="true" id="link2">
GitHub 链接
</a>
<!-- 拖放区域 -->
<div class="drop-zone" id="dropZone">
<p>拖放链接到这里</p>
</div>
<script>
// 拖拽链接时,浏览器会自动设置 text/uri-list
document.querySelectorAll('.link-item').forEach(link => {
link.addEventListener('dragstart', function(e) {
// 浏览器会自动设置 text/uri-list 为链接的 href
// 也可以手动设置
e.dataTransfer.setData('text/uri-list', this.href);
e.dataTransfer.setData('text/plain', this.href);
console.log('拖拽链接:', this.href);
});
});
const dropZone = document.getElementById('dropZone');
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'link';
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
// 获取 URI 列表
const uriList = e.dataTransfer.getData('text/uri-list');
if (uriList) {
// 处理 URI 列表(可能包含多个 URI)
const uris = uriList.split('\n').filter(uri => {
// 过滤掉注释和空行
return uri.trim() && !uri.startsWith('#');
});
console.log('接收到的 URI:', uris);
// 显示接收到的链接
uris.forEach(uri => {
const linkDiv = document.createElement('div');
linkDiv.className = 'received-link';
linkDiv.innerHTML = `
<strong>接收到链接:</strong><br>
<a href="${uri}" target="_blank">${uri}</a>
`;
this.appendChild(linkDiv);
});
}
});
</script>
</body>
</html>从浏览器地址栏拖拽:
<!DOCTYPE html>
<html>
<head>
<style>
.drop-zone {
min-height: 200px;
border: 3px dashed #999;
padding: 40px;
text-align: center;
margin: 20px;
background: #f9f9f9;
}
.drop-zone.drag-over {
background: #e3f2fd;
border-color: #2196F3;
}
</style>
</head>
<body>
<h2>从浏览器地址栏拖拽 URL</h2>
<p>尝试从浏览器地址栏或书签栏拖拽 URL 到下方区域</p>
<div class="drop-zone" id="urlDropZone">
<p style="font-size: 48px;">🔗</p>
<p>拖拽 URL 到这里</p>
</div>
<div id="result"></div>
<script>
const urlDropZone = document.getElementById('urlDropZone');
const result = document.getElementById('result');
urlDropZone.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
urlDropZone.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'link';
});
urlDropZone.addEventListener('dragleave', function(e) {
this.classList.remove('drag-over');
});
urlDropZone.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
// 获取 URI 列表
const uriList = e.dataTransfer.getData('text/uri-list');
const plainText = e.dataTransfer.getData('text/plain');
console.log('URI List:', uriList);
console.log('Plain Text:', plainText);
if (uriList) {
// 解析 URI 列表
const uris = uriList.split('\n').filter(uri => {
return uri.trim() && !uri.startsWith('#');
});
result.innerHTML = `
<h3>接收到的 URL:</h3>
<ul>
${uris.map(uri => `
<li>
<a href="${uri}" target="_blank">${uri}</a>
</li>
`).join('')}
</ul>
`;
} else if (plainText) {
result.innerHTML = `
<h3>接收到的文本:</h3>
<p>${plainText}</p>
`;
}
});
</script>
</body>
</html>多个 URI 的格式:
// 设置多个 URI
const uriList = `https://www.example.com
https://www.github.com
# 这是注释
https://www.google.com`;
e.dataTransfer.setData('text/uri-list', uriList);
// 接收时解析
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
const uriList = e.dataTransfer.getData('text/uri-list');
// 解析 URI 列表
const uris = uriList.split('\n').filter(line => {
const trimmed = line.trim();
// 过滤空行和注释
return trimmed && !trimmed.startsWith('#');
});
console.log('解析出的 URI:', uris);
// ['https://www.example.com', 'https://www.github.com', 'https://www.google.com']
});浏览器支持情况:
| 浏览器 | 支持版本 | 说明 |
|---|---|---|
| Chrome | ✅ 所有版本 | 完全支持 |
| Firefox | ✅ 所有版本 | 完全支持 |
| Safari | ✅ 所有版本 | 完全支持 |
| Edge | ✅ 所有版本 | 完全支持 |
| IE | ✅ IE 10+ | 部分支持 |
常见使用场景:
- 拖拽浏览器书签
- 拖拽地址栏 URL
- 拖拽网页中的链接
- 拖拽邮件客户端中的链接
- 拖拽文件管理器中的网络位置
注意事项:
text/uri-list只能包含 URI,不能包含其他数据- 每个 URI 必须是完整的绝对 URI(包含协议)
- 相对 URI 可能在某些浏览器中不被支持
- 建议同时设置
text/plain作为备用
八、高级功能
8.1 自定义拖拽图像
<!DOCTYPE html>
<html>
<head>
<style>
.drag-item {
padding: 20px;
background: #2196F3;
color: white;
cursor: move;
margin: 20px;
display: inline-block;
}
.custom-drag-image {
padding: 15px;
background: #FF5722;
color: white;
border-radius: 8px;
position: absolute;
left: -9999px;
}
</style>
</head>
<body>
<div class="drag-item" draggable="true" id="item">
拖动我(自定义拖拽图像)
</div>
<!-- 自定义拖拽图像(隐藏) -->
<div class="custom-drag-image" id="customImage">
🎯 正在拖拽...
</div>
<div style="min-height: 200px; border: 2px dashed #999; margin: 20px; padding: 20px;" id="dropZone">
拖放区域
</div>
<script>
const item = document.getElementById('item');
const customImage = document.getElementById('customImage');
const dropZone = document.getElementById('dropZone');
item.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', 'Custom Image Demo');
// 设置自定义拖拽图像
// 参数: (元素, x偏移, y偏移)
e.dataTransfer.setDragImage(customImage, 50, 25);
});
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
const data = e.dataTransfer.getData('text/plain');
this.innerHTML = `<p>接收到: ${data}</p>`;
});
</script>
</body>
</html>8.2 拖拽时的视觉反馈
<!DOCTYPE html>
<html>
<head>
<style>
.item {
padding: 15px;
margin: 10px;
background: #4CAF50;
color: white;
cursor: move;
transition: opacity 0.3s;
}
.item.dragging {
opacity: 0.5;
border: 2px dashed #fff;
}
.drop-zone {
min-height: 150px;
border: 2px dashed #999;
margin: 10px;
padding: 20px;
transition: all 0.3s;
}
.drop-zone.drag-over {
background: #e3f2fd;
border-color: #2196F3;
border-width: 3px;
transform: scale(1.02);
}
</style>
</head>
<body>
<div class="item" draggable="true">项目 1</div>
<div class="item" draggable="true">项目 2</div>
<div class="item" draggable="true">项目 3</div>
<div class="drop-zone">拖放区域</div>
<script>
const items = document.querySelectorAll('.item');
const dropZone = document.querySelector('.drop-zone');
items.forEach(item => {
// 拖拽开始 - 添加视觉效果
item.addEventListener('dragstart', function(e) {
this.classList.add('dragging');
e.dataTransfer.setData('text/plain', this.textContent);
});
// 拖拽结束 - 移除视觉效果
item.addEventListener('dragend', function(e) {
this.classList.remove('dragging');
});
});
dropZone.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
});
dropZone.addEventListener('dragleave', function(e) {
this.classList.remove('drag-over');
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
const data = e.dataTransfer.getData('text/plain');
const newItem = document.createElement('div');
newItem.style.cssText = 'padding: 10px; margin: 5px; background: #4CAF50; color: white;';
newItem.textContent = data;
this.appendChild(newItem);
});
</script>
</body>
</html>九、在 Vue 框架中使用注意事项
9.1 Vue 2 中的使用
基本用法
<template>
<div>
<!-- 拖拽源 -->
<div
v-for="item in items"
:key="item.id"
draggable="true"
@dragstart="handleDragStart(item)"
>
{{ item.name }}
</div>
<!-- 拖放目标 -->
<div
class="drop-zone"
@dragover.prevent
@drop="handleDrop"
>
拖放区域
</div>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, name: '项目 1' },
{ id: 2, name: '项目 2' }
],
dragData: null // 使用组件状态存储拖拽数据
};
},
methods: {
handleDragStart(item) {
// 直接保存到组件状态
this.dragData = item;
},
handleDrop(event) {
event.preventDefault();
if (this.dragData) {
console.log('接收到:', this.dragData);
// 处理数据...
}
this.dragData = null;
}
}
};
</script>Vue 2 注意事项
- 必须使用 .prevent 修饰符
<!-- ✅ 正确 -->
<div @dragover.prevent="handleDragOver"></div>
<!-- ❌ 错误:drop 事件不会触发 -->
<div @dragover="handleDragOver"></div>- 响应式数据更新
// ❌ 错误:直接修改数组可能不触发更新
this.items.push(newItem);
// ✅ 正确:使用 Vue.set 或替换整个数组
this.items = [...this.items, newItem];
// 或
this.$set(this.items, this.items.length, newItem);- 数据传递推荐方式
在 Vue 2 中,推荐使用组件状态而不是 dataTransfer:
// ✅ 推荐:使用组件状态
data() {
return {
dragData: null
};
},
methods: {
handleDragStart(item) {
this.dragData = item; // 直接保存
},
handleDrop(event) {
event.preventDefault();
console.log(this.dragData); // 直接使用
}
}9.2 Vue 3 中的使用
组合式 API (Composition API)
<template>
<div>
<!-- 拖拽源 -->
<div
v-for="item in items"
:key="item.id"
draggable="true"
@dragstart="handleDragStart(item)"
>
{{ item.name }}
</div>
<!-- 拖放目标 -->
<div
class="drop-zone"
@dragover.prevent
@drop="handleDrop"
>
拖放区域
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const items = ref([
{ id: 1, name: '项目 1' },
{ id: 2, name: '项目 2' }
]);
const dragData = ref(null);
const handleDragStart = (item) => {
dragData.value = item;
};
const handleDrop = (event) => {
event.preventDefault();
if (dragData.value) {
console.log('接收到:', dragData.value);
// 处理数据
items.value.push({ ...dragData.value });
}
dragData.value = null;
};
</script>Vue 3 注意事项
- 响应式引用
// ✅ 正确:使用 .value 访问 ref
const dragData = ref(null);
dragData.value = item;
// ❌ 错误:忘记 .value
dragData = item;- 响应式数组操作
// ✅ Vue 3 中直接修改数组是响应式的
items.value.push(newItem);
items.value.splice(index, 1);
// 也可以替换整个数组
items.value = [...items.value, newItem];- 使用 toRefs 解构
import { reactive, toRefs } from 'vue';
const state = reactive({
items: [],
draggedItem: null
});
// ✅ 使用 toRefs 保持响应式
const { items, draggedItem } = toRefs(state);- 数据传递推荐方式(重要!)
在 Vue 3 中,同样推荐使用组件状态而不是 dataTransfer:
<template>
<div>
<!-- 拖拽源 -->
<div
v-for="item in sourceItems"
:key="item.id"
draggable="true"
@dragstart="handleDragStart(item)"
>
{{ item.name }}
</div>
<!-- 拖放目标 -->
<div
class="drop-zone"
@dragover.prevent
@drop="handleDrop"
>
拖放区域
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const sourceItems = ref([
{ id: 1, name: '项目 1', metadata: { /* 复杂对象 */ } },
{ id: 2, name: '项目 2', metadata: { /* 复杂对象 */ } }
]);
// 使用 ref 存储拖拽数据
const dragData = ref(null);
// ✅ 推荐:直接使用组件状态
const handleDragStart = (item) => {
// 直接保存,无需序列化
dragData.value = item;
// 不需要使用 dataTransfer!
};
const handleDrop = (event) => {
event.preventDefault();
// 直接使用,无需反序列化
if (dragData.value) {
console.log('接收到数据:', dragData.value);
console.log('可以直接访问复杂对象:', dragData.value.metadata);
// 处理数据...
}
// 清空
dragData.value = null;
};
</script>9.3 Vue 通用最佳实践
- 什么时候必须使用 dataTransfer?
只有以下场景才需要使用 dataTransfer:
// ❌ 场景 1: 跨窗口拖拽(必须使用 dataTransfer)
methods: {
handleDragStart(event, item) {
// 跨窗口无法访问组件状态,必须序列化
event.dataTransfer.setData('application/json', JSON.stringify(item));
},
handleDrop(event) {
const data = JSON.parse(event.dataTransfer.getData('application/json'));
}
}
// ❌ 场景 2: 拖拽文件(必须使用 dataTransfer.files)
methods: {
handleDrop(event) {
event.preventDefault();
const files = event.dataTransfer.files;
console.log('接收到文件:', files);
}
}
// ❌ 场景 3: 从外部拖拽内容(如浏览器地址栏、其他应用)
methods: {
handleDrop(event) {
event.preventDefault();
// 获取拖拽的 URL
const url = event.dataTransfer.getData('text/uri-list');
// 获取拖拽的文本
const text = event.dataTransfer.getData('text/plain');
}
}- 对比总结
<!-- ✅ 推荐:同一父组件内使用组件状态 -->
<script>
export default {
data() {
return {
dragData: null // 简单直接
};
},
methods: {
onDragStart(item) {
this.dragData = item; // 无需序列化
},
onDrop() {
console.log(this.dragData); // 直接使用
}
}
};
</script>
<!-- ❌ 不推荐:同一父组件内使用 dataTransfer -->
<script>
export default {
methods: {
onDragStart(event, item) {
// 需要序列化,代码冗余
event.dataTransfer.setData('application/json', JSON.stringify(item));
},
onDrop(event) {
// 需要反序列化,容易出错
const data = JSON.parse(event.dataTransfer.getData('application/json'));
}
}
};
</script>十、在 React 框架中使用注意事项
10.1 React 基本用法
import React, { useState } from 'react';
function DragDropExample() {
const [items] = useState([
{ id: 1, name: '项目 1' },
{ id: 2, name: '项目 2' }
]);
const [draggedItem, setDraggedItem] = useState(null);
const [droppedItems, setDroppedItems] = useState([]);
const handleDragStart = (event, item) => {
setDraggedItem(item);
event.dataTransfer.effectAllowed = 'copy';
// 也可以使用 dataTransfer
event.dataTransfer.setData('application/json', JSON.stringify(item));
};
const handleDragEnd = () => {
setDraggedItem(null);
};
const handleDragOver = (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
};
const handleDrop = (event) => {
event.preventDefault();
if (draggedItem) {
setDroppedItems([...droppedItems, draggedItem]);
setDraggedItem(null);
}
};
return (
<div>
<div>
<h3>拖拽源</h3>
{items.map(item => (
<div
key={item.id}
draggable
onDragStart={(e) => handleDragStart(e, item)}
onDragEnd={handleDragEnd}
style={{
padding: '10px',
margin: '5px',
background: '#4CAF50',
color: 'white',
cursor: 'move'
}}
>
{item.name}
</div>
))}
</div>
<div
onDragOver={handleDragOver}
onDrop={handleDrop}
style={{
minHeight: '200px',
border: '2px dashed #999',
padding: '20px',
marginTop: '20px'
}}
>
<h3>拖放区域</h3>
{droppedItems.map((item, index) => (
<div key={index} style={{ padding: '10px', background: '#f0f0f0', margin: '5px' }}>
{item.name}
</div>
))}
</div>
</div>
);
}
export default DragDropExample;十一、常见问题汇总
问题 1: drop 事件不触发
原因: 没有在 dragover 中调用 preventDefault()
// ✅ 正确
element.addEventListener('dragover', (e) => {
e.preventDefault();
});问题 2: 无法传递对象数据
原因: dataTransfer 只能传递字符串
// ✅ 正确:序列化对象
e.dataTransfer.setData('application/json', JSON.stringify(obj));
// 接收时反序列化
const obj = JSON.parse(e.dataTransfer.getData('application/json'));问题 3: 拖拽图像显示不正确
解决方案: 使用 setDragImage 自定义
e.dataTransfer.setDragImage(customElement, offsetX, offsetY);问题 4: 移动端不支持
解决方案: 需要额外处理 touch 事件或使用第三方库
// 监听 touch 事件模拟拖拽
element.addEventListener('touchstart', handleTouchStart);
element.addEventListener('touchmove', handleTouchMove);
element.addEventListener('touchend', handleTouchEnd);问题 5: 跨域拖拽限制
解决方案: 只能在同源窗口间拖拽,跨域需要其他方案(如 postMessage)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。