HTML5 Drag & Drop API 完整指南
HTML5 Drag & Drop API 是浏览器原生提供的拖拽功能,允许用户通过鼠标拖动元素并将其放置到目标位置。 在 在 由于 HTML5 Drag & Drop API 支持在同源的不同窗口/标签页之间拖拽: HTML5 Drag & Drop API 支持多种标准 MIME 类型: 格式规范: 使用示例: 从浏览器地址栏拖拽: 多个 URI 的格式: 浏览器支持情况: 常见使用场景: 注意事项: 在 Vue 2 中,推荐使用组件状态而不是 dataTransfer: 在 Vue 3 中,同样推荐使用组件状态而不是 dataTransfer: 只有以下场景才需要使用 dataTransfer: 原因: 没有在 dragover 中调用 preventDefault() 原因: dataTransfer 只能传递字符串 解决方案: 使用 setDragImage 自定义 解决方案: 需要额外处理 touch 事件或使用第三方库 解决方案: 只能在同源窗口间拖拽,跨域需要其他方案(如 postMessage)一、基础概念
核心角色
二、完整的拖拽流程
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 关键点说明
dragover 和 drop 中必须调用,否则拖放无效四、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 同源窗口间拖拽
<!-- 窗口 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 常用数据类型说明
数据类型 说明 使用场景 浏览器支持 text/plain纯文本 拖拽文本内容 ✅ 所有浏览器 text/htmlHTML 内容 拖拽富文本 ✅ 所有浏览器 text/uri-listURI 列表 拖拽链接、书签 ✅ 所有浏览器 application/jsonJSON 数据 自定义数据传递 ✅ 所有浏览器 Files文件对象 拖拽文件 ✅ 所有浏览器 text/uri-list 详解
text/uri-list 是一个标准的 MIME 类型,用于传递一个或多个 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
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+ 部分支持 text/uri-list 只能包含 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 注意事项
<!-- ✅ 正确 -->
<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);// ✅ 推荐:使用组件状态
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];import { reactive, toRefs } from 'vue';
const state = reactive({
items: [],
draggedItem: null
});
// ✅ 使用 toRefs 保持响应式
const { items, draggedItem } = toRefs(state);<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 通用最佳实践
// ❌ 场景 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 事件不触发
// ✅ 正确
element.addEventListener('dragover', (e) => {
e.preventDefault();
});问题 2: 无法传递对象数据
// ✅ 正确:序列化对象
e.dataTransfer.setData('application/json', JSON.stringify(obj));
// 接收时反序列化
const obj = JSON.parse(e.dataTransfer.getData('application/json'));问题 3: 拖拽图像显示不正确
e.dataTransfer.setDragImage(customElement, offsetX, offsetY);问题 4: 移动端不支持
// 监听 touch 事件模拟拖拽
element.addEventListener('touchstart', handleTouchStart);
element.addEventListener('touchmove', handleTouchMove);
element.addEventListener('touchend', handleTouchEnd);问题 5: 跨域拖拽限制
十二、参考资料