一、基础概念

HTML5 Drag & Drop API 是浏览器原生提供的拖拽功能,允许用户通过鼠标拖动元素并将其放置到目标位置。

核心角色

  1. 拖拽源(Drag Source):可以被拖动的元素
  2. 拖放目标(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 关键点说明

  1. draggable="true":使元素可拖拽
  2. e.preventDefault():在 dragoverdrop 中必须调用,否则拖放无效
  3. 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 跨窗口拖拽的限制

  1. 同源策略:只能在同源(相同协议、域名、端口)的窗口间拖拽
  2. 数据类型限制:只能传递字符串数据,复杂对象需要序列化
  3. 安全限制:某些浏览器可能限制跨窗口拖拽功能

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/htmlHTML 内容拖拽富文本✅ 所有浏览器
text/uri-listURI 列表拖拽链接、书签✅ 所有浏览器
application/jsonJSON 数据自定义数据传递✅ 所有浏览器
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+部分支持

常见使用场景:

  1. 拖拽浏览器书签
  2. 拖拽地址栏 URL
  3. 拖拽网页中的链接
  4. 拖拽邮件客户端中的链接
  5. 拖拽文件管理器中的网络位置

注意事项:

  1. text/uri-list 只能包含 URI,不能包含其他数据
  2. 每个 URI 必须是完整的绝对 URI(包含协议)
  3. 相对 URI 可能在某些浏览器中不被支持
  4. 建议同时设置 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 注意事项

  1. 必须使用 .prevent 修饰符
<!-- ✅ 正确 -->
<div @dragover.prevent="handleDragOver"></div>

<!-- ❌ 错误:drop 事件不会触发 -->
<div @dragover="handleDragOver"></div>
  1. 响应式数据更新
// ❌ 错误:直接修改数组可能不触发更新
this.items.push(newItem);

// ✅ 正确:使用 Vue.set 或替换整个数组
this.items = [...this.items, newItem];
// 或
this.$set(this.items, this.items.length, newItem);
  1. 数据传递推荐方式

在 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 注意事项

  1. 响应式引用
// ✅ 正确:使用 .value 访问 ref
const dragData = ref(null);
dragData.value = item;

// ❌ 错误:忘记 .value
dragData = item;
  1. 响应式数组操作
// ✅ Vue 3 中直接修改数组是响应式的
items.value.push(newItem);
items.value.splice(index, 1);

// 也可以替换整个数组
items.value = [...items.value, newItem];
  1. 使用 toRefs 解构
import { reactive, toRefs } from 'vue';

const state = reactive({
  items: [],
  draggedItem: null
});

// ✅ 使用 toRefs 保持响应式
const { items, draggedItem } = toRefs(state);
  1. 数据传递推荐方式(重要!)

在 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 通用最佳实践

  1. 什么时候必须使用 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');
  }
}
  1. 对比总结
<!-- ✅ 推荐:同一父组件内使用组件状态 -->
<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)

十二、参考资料

  1. MDN - HTML Drag and Drop API
  2. MDN - DataTransfer
  3. Vue 3 官方文档
  4. React 官方文档

不可能的是
2.3k 声望123 粉丝

stay hungry&&foolish