1. 拖拽概述
拖拽交互在前端应用中无处不在:文件上传、列表排序、拖拽布局、可视化编辑器等。
前端实现拖拽的方式主要有两种:
- 原生 HTML5 拖拽 API :浏览器内置的拖拽能力
- 自定义拖拽 :基于鼠标/触摸事件实现的拖拽功能
每种方式都有其适用场景和优缺点,本文就来说说前端拖拽中的那些事儿。
2. 原生拖拽 API
浏览器中有些元素默认就是可拖拽的,比如选中的文本、图片、链接;也有些元素默认是可放置的,比如输入框默认也可以作为文本的可放置元素。

HTML5引入了原生拖拽API,为Web应用提供了标准化的拖拽交互支持。核心概念包括:
可拖拽元素:通过设置 draggable="true"
属性使元素可拖拽
拖拽事件:包括 dragstart、drag、dragend、dragenter、dragover、dragleave 和 drop
数据传输对象:DataTransfer 接口用于在拖拽操作中存储和传递数据
2.1 基础用法
要使元素可拖拽,需要设置 draggable 属性:
<div draggable="true">我可以被拖拽</div>
拖放涉及到两种元素,一种是被拖拽元素(drag source,源对象),一种是放置区元素(drop target,目标对象)。如下图所示,鼠标长按拖拽A元素放置到B元素上,A元素即为源对象,B元素即为目标对象

2.2 拖拽事件
不同的对象产生不同的拖放事件。
触发对象 | 事件名称 | 说明 |
---|
源对象 | dragstart | 源对象开始被拖动时触发 |
| drag | 源对象被拖动过程中反复触发 |
| dragend | 源对象拖动结束时触发 |
目标对象 | dragenter | 源对象开始进入目标对象范围内触发 |
| dragover | 源对象在目标对象范围内移动时触发 |
| dragleave | 源对象离开目标对象范围时触发 |
| drop | 源对象在目标对象范围内被释放时触发 |
对于被拖拽元素,事件触发顺序是:
graph LR
dragstart --> drag --> dragend
对于目标元素,事件触发的顺序是
graph LR
dragenter --> dragover --> drop/dropleave
其中drag
和dragover
会分别在源元素和目标元素反复触发。整个流程一定是dragstart
第一个触发,dragend
最后一个触发。
如果某个元素同时设置了dragover
和drop
的监听,那么必须阻止dragover
的默认行为,否则drop
将不会被触发。
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
2.3 DataTansfer
DataTransfer 对象如同它的名字,作用就是在拖放过程中对数据进行传输,实际上它包含了拖拽事件的状态,例如拖拽事件的类型(如拷贝 copy 或者移动 move),拖拽的数据(一个或者多个项)和每个拖拽项的类型(MIME 类型)。
定义拖拽数据
setData
用来存放数据,getData
用来获取数据,出于安全的考量,数据只能在drop
时获取
draggable.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', e.target.id);
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
const id = e.dataTransfer.getData('text/plain');
});
定义拖拽过程中的视觉效果
dropEffect
属性会影响到拖拽过程中浏览器显示的鼠标样式
e.dataTransfer.dropEffect = 'copy';
拖拽过程中,浏览器会在鼠标旁显示一张默认图片,可以通过 setDragImage
方法自定义一张图片
e.dataTransfer.setDragImage(imgElement, xOffset, yOffset);
2.4 完整示例
下面是一个简单的拖拽示例,可以将可拖动的方块拖入目标区域。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>基础拖放示例</title>
<style>
#draggable {
width: 100px;
height: 100px;
background-color: #3498db;
color: white;
text-align: center;
line-height: 100px;
cursor: move;
}
#dropzone {
width: 300px;
height: 300px;
border: 2px dashed #2c3e50;
margin-top: 20px;
display: flex;
justify-content: center;
align-items: center;
}
.drop-active {
background-color: #ecf0f1;
}
</style>
</head>
<body>
<div id="draggable" draggable="true">拖动我</div>
<div id="dropzone">放置区域</div>
<script>
const draggable = document.getElementById('draggable');
const dropzone = document.getElementById('dropzone');
draggable.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', e.target.id);
e.dataTransfer.effectAllowed = 'move';
});
dropzone.addEventListener('dragenter', (e) => {
e.preventDefault();
dropzone.classList.add('drop-active');
});
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('drop-active');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('drop-active');
const id = e.dataTransfer.getData('text/plain');
const draggableElement = document.getElementById(id);
dropzone.appendChild(draggableElement);
});
</script>
</body>
</html>

3. 自定义拖拽
虽然原生 API 使用简单,但存在一些局限,比如移动设备支持不佳、拖拽过程中的视觉反馈有限等。为了克服这些的局限,我们可以基于鼠标/触摸事件实现自定义拖拽。
3.1 核心原理
自定义拖拽的核心是捕获以下事件:
- 鼠标事件:
mousedown
、 mousemove
、 mouseup
- 触摸事件:
touchstart
、 touchmove
、 touchend
3.2 实现步骤
- 监听元素的
mousedown
/ touchstart
事件 - 记录初始位置和偏移量
- 监听
mousemove
/ touchmove
事件,更新元素位置 - 监听
mouseup
/ touchend
事件,结束拖拽
3.3 完整示例
下面是一个自定义的拖拽实现,允许用户在限定区域内拖动一个色块,实现的思路如下:
- 定义了一个可拖拽的元素
.draggable
,并设置了样式和初始状态。 - 容器
.container
限制了拖拽范围,超出部分会被隐藏。 - 使用
mousedown
和 touchstart
事件监听拖拽开始,计算鼠标或触控点相对于元素的位置偏移量。 - 在拖拽过程中,通过
mousemove
或 touchmove
更新元素位置,并进行边界检测以确保元素不会移出容器。 - 拖拽结束时移除相关事件监听,并恢复元素样式。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自定义拖拽示例</title>
<style>
.draggable {
width: 100px;
height: 100px;
background-color: #3498db;
color: white;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
cursor: move;
user-select: none;
touch-action: none;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.dragging {
opacity: 0.8;
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
z-index: 1000;
}
.container {
width: 25%;
height: 400px;
border: 2px dashed #ccc;
position: relative;
overflow: hidden;
}
</style>
</head>
<body>
<h2>自定义拖拽</h2>
<div class="container">
<div class="draggable" id="draggable1">拖拽我</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const draggables = document.querySelectorAll('.draggable');
draggables.forEach(draggable => {
let offsetX, offsetY;
let isDragging = false;
function handleStart(e) {
const event = e.type === 'touchstart' ? e.touches[0] : e;
const rect = draggable.getBoundingClientRect();
offsetX = event.clientX - rect.left;
offsetY = event.clientY - rect.top;
isDragging = true;
draggable.classList.add('dragging');
if (e.type === 'mousedown') {
document.addEventListener('mousemove', handleMove);
document.addEventListener('mouseup', handleEnd);
}
}
function handleMove(e) {
if (!isDragging) return;
e.preventDefault();
const event = e.type === 'touchmove' ? e.touches[0] : e;
const container = document.querySelector('.container');
const containerRect = container.getBoundingClientRect();
let left = event.clientX - containerRect.left - offsetX;
let top = event.clientY - containerRect.top - offsetY;
const maxLeft = containerRect.width - draggable.offsetWidth;
const maxTop = containerRect.height - draggable.offsetHeight;
left = Math.max(0, Math.min(left, maxLeft));
top = Math.max(0, Math.min(top, maxTop));
draggable.style.left = `${left}px`;
draggable.style.top = `${top}px`;
}
function handleEnd() {
if (!isDragging) return;
isDragging = false;
draggable.classList.remove('dragging');
document.removeEventListener('mousemove', handleMove);
document.removeEventListener('mouseup', handleEnd);
document.removeEventListener('touchmove', handleMove);
document.removeEventListener('touchend', handleEnd);
}
draggable.addEventListener('mousedown', handleStart);
draggable.addEventListener('touchstart', e => {
handleStart(e);
document.addEventListener('touchmove', handleMove, { passive: false });
document.addEventListener('touchend', handleEnd);
});
});
});
</script>
</body>
</html>

4. 常见拖拽功能
在实际应用中,我们常需要一些高级拖拽功能,如拖拽排序、拖拽上传等。
4.1 拖拽排序
拖拽排序是最常见的应用场景之一,其实现思路大体如下:
(1)开始拖拽
当鼠标按下时,记录被拖拽元素,计算鼠标偏移量,创建占位符并插入到被拖拽元素后,设置被拖拽元素样式为绝对定位,同时为 document
绑定 mousemove
和 mouseup
事件。
(2)拖拽过程
若鼠标移动,根据鼠标位置更新被拖拽元素位置。
隐藏被拖拽元素以获取鼠标下方元素,再显示被拖拽元素。
判断下方元素是否为可排序项,若是则计算其中点位置,根据鼠标位置与中点位置的关系,决定将占位符插入到可排序项前或后。
(3)结束拖拽
当鼠标释放,移除 mousemove
和 mouseup
事件监听器,恢复被拖拽元素默认样式,将被拖拽元素插入到占位符位置,移除占位符元素,完成拖拽排序。
下面是一个示例:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>拖拽排序</title>
<style>
.sortable-container {
width: 300px;
border: 1px solid #ddd;
padding: 10px;
}
.sortable-item {
padding: 10px;
margin: 5px 0;
background-color: #f9f9f9;
border: 1px solid #eee;
cursor: move;
transition: transform 0.2s, box-shadow 0.2s;
}
.sortable-item.dragging {
background-color: #f0f0f0;
box-shadow: 0 5px 10px rgba(0,0,0,0.1);
transform: scale(1.02);
z-index: 1;
}
.placeholder {
height: 40px;
background-color: #e9e9e9;
border: 2px dashed #ccc;
}
</style>
</head>
<body>
<h2>拖拽排序列表</h2>
<div class="sortable-container" id="sortable">
<div class="sortable-item">项目 1</div>
<div class="sortable-item">项目 2</div>
<div class="sortable-item">项目 3</div>
<div class="sortable-item">项目 4</div>
<div class="sortable-item">项目 5</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const sortable = document.getElementById('sortable');
const items = sortable.querySelectorAll('.sortable-item');
let draggedItem = null;
let placeholder = document.createElement('div');
placeholder.className = 'placeholder';
items.forEach(item => {
item.addEventListener('mousedown', function(e) {
e.preventDefault();
draggedItem = this;
const rect = draggedItem.getBoundingClientRect();
const offsetX = e.clientX - rect.left;
const offsetY = e.clientY - rect.top;
placeholder.style.height = `${rect.height}px`;
draggedItem.parentNode.insertBefore(placeholder, draggedItem.nextSibling);
draggedItem.classList.add('dragging');
draggedItem.style.position = 'absolute';
draggedItem.style.zIndex = 1000;
draggedItem.style.width = `${rect.width}px`;
function moveAt(clientX, clientY) {
draggedItem.style.left = `${clientX - offsetX}px`;
draggedItem.style.top = `${clientY - offsetY}px`;
}
moveAt(e.clientX, e.clientY);
function onMouseMove(e) {
moveAt(e.clientX, e.clientY);
draggedItem.style.display = 'none';
const elemBelow = document.elementFromPoint(e.clientX, e.clientY);
draggedItem.style.display = 'block';
if (!elemBelow) return;
const droppable = elemBelow.closest('.sortable-item');
if (droppable && droppable !== draggedItem) {
const droppableRect = droppable.getBoundingClientRect();
const droppableMidY = droppableRect.top + droppableRect.height / 2;
if (e.clientY < droppableMidY) {
droppable.parentNode.insertBefore(placeholder, droppable);
} else {
droppable.parentNode.insertBefore(placeholder, droppable.nextSibling);
}
}
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', function onMouseUp() {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
draggedItem.style.position = '';
draggedItem.style.left = '';
draggedItem.style.top = '';
draggedItem.style.width = '';
draggedItem.style.zIndex = '';
draggedItem.classList.remove('dragging');
placeholder.parentNode.insertBefore(draggedItem, placeholder);
placeholder.remove();
draggedItem = null;
}, { once: true });
});
item.addEventListener('dragstart', e => e.preventDefault());
});
});
</script>
</body>
</html>

4.2 拖拽调整大小
拖拽调整元素大小是另一个常见需求,实现的思路大体如下:
(1)开始调整
当鼠标按下手柄时,阻止默认行为,将 isResizing
标记为 true
,同时记录鼠标按下时的位置和元素的初始宽度、高度。
(2)调整过程
若鼠标移动,检查 isResizing
是否为 true
,如果是则计算新的宽度和高度。
分别判断新宽度和新高度是否大于最小尺寸(如100px),根据判断结果更新元素的宽度和高度样式。
(3)结束调整
当鼠标释放时,将 isResizing
标记为 false
。
下面是一个示例:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>拖拽调整大小</title>
<style>
.resizable {
width: 200px;
height: 150px;
background-color: #3498db;
color: white;
position: relative;
padding: 20px;
box-sizing: border-box;
overflow: hidden;
}
.resize-handle {
width: 10px;
height: 10px;
background-color: white;
position: absolute;
right: 0;
bottom: 0;
cursor: nwse-resize;
}
</style>
</head>
<body>
<h2>拖拽调整大小</h2>
<div class="resizable" id="resizable">
<div class="content">可调整大小的元素</div>
<div class="resize-handle" id="resize-handle"></div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const resizable = document.getElementById('resizable');
const handle = document.getElementById('resize-handle');
let isResizing = false;
let startX, startY, startWidth, startHeight;
handle.addEventListener('mousedown', function(e) {
e.preventDefault();
isResizing = true;
startX = e.clientX;
startY = e.clientY;
startWidth = parseInt(document.defaultView.getComputedStyle(resizable).width, 10);
startHeight = parseInt(document.defaultView.getComputedStyle(resizable).height, 10);
document.addEventListener('mousemove', resize);
document.addEventListener('mouseup', stopResize);
});
function resize(e) {
if (!isResizing) return;
const width = startWidth + e.clientX - startX;
const height = startHeight + e.clientY - startY;
if (width > 100) resizable.style.width = `${width}px`;
if (height > 100) resizable.style.height = `${height}px`;
}
function stopResize() {
isResizing = false;
document.removeEventListener('mousemove', resize);
document.removeEventListener('mouseup', stopResize);
}
});
</script>
</body>
</html>

4.3 拖拽上传
拖拽上传文件是现代 Web 应用的常见功能,实现思路如下:
(1)交互阶段
用户可以选择将文件拖拽到拖拽区域,或者点击选择按钮选择文件。 当有文件拖放或选择时,获取文件列表。
(2)文件处理阶段
遍历文件列表,为每个文件创建文件项元素,设置文件名和文件大小,并将其添加到文件列表中。
下面是一个示例:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>拖拽上传</title>
<style>
.drop-area {
width: 300px;
height: 200px;
border: 3px dashed #ccc;
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: all 0.3s;
margin: 20px 0;
}
.drop-area.active {
border-color: #3498db;
background-color: rgba(52, 152, 219, 0.1);
}
.file-list {
margin-top: 20px;
width: 300px;
}
.file-item {
display: flex;
justify-content: space-between;
padding: 8px;
border: 1px solid #eee;
margin-bottom: 5px;
border-radius: 4px;
}
.file-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-size {
margin-left: 10px;
color: #666;
}
</style>
</head>
<body>
<h2>拖拽上传文件</h2>
<div class="drop-area" id="drop-area">
<p>拖拽文件到此处上传</p>
<p>或</p>
<input type="file" id="file-input" multiple style="display: none;">
<button id="select-button">选择文件</button>
</div>
<div class="file-list" id="file-list">
<h3>已选文件:</h3>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const dropArea = document.getElementById('drop-area');
const fileInput = document.getElementById('file-input');
const selectButton = document.getElementById('select-button');
const fileList = document.getElementById('file-list');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, unhighlight, false);
});
function highlight() {
dropArea.classList.add('active');
}
function unhighlight() {
dropArea.classList.remove('active');
}
dropArea.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
handleFiles(files);
}
selectButton.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', () => {
handleFiles(fileInput.files);
});
function handleFiles(files) {
if (files.length === 0) return;
Array.from(files).forEach(file => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
const fileName = document.createElement('div');
fileName.className = 'file-name';
fileName.textContent = file.name;
const fileSize = document.createElement('div');
fileSize.className = 'file-size';
fileSize.textContent = formatFileSize(file.size);
fileItem.appendChild(fileName);
fileItem.appendChild(fileSize);
fileList.appendChild(fileItem);
});
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
fetch('your-upload-url', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
console.log('上传成功:', data);
})
.catch(error => {
console.error('上传失败:', error);
});
}
});
</script>
</body>
</html>

5. 通用拖拽解决方案
在实际项目中,我们通常会使用成熟的拖拽库来简化开发,这里列举了一些流行的拖拽库。
拖拽实现方式 | 基础拖拽能力表现 | 框架兼容 | 实现原理 |
---|
React DnD | 支持元素的拖拽和放置 | React | HTML5 |
react-grid-layout | 支持网格/自由拖拽和缩放,不支持旋转 | React | 鼠标事件 |
react-draggable | 支持自由拖拽,不支持缩放和旋转 | React | 鼠标事件 |
react-resizable | 支持缩放,不支持自由拖拽和旋转 | React | 鼠标事件 |
Vue Draggable | 支持自由拖拽和缩放 | Vue2 | 鼠标事件 |
vue-drag-resize | 支持自由拖拽和缩放 | Vue2、Vue3 | 鼠标事件 |
Vue3 Dnd | 支持元素的拖拽和放置 | Vue3 | HTML5 |
Vue Grid Layout | 支持网格/自由拖拽和缩放,不支持旋转 | Vue2、Vue3 | 鼠标事件 |
dnd/kit | 支持元素的拖拽和放置 | Vue和React | HTML5 |
movable | 支持自由拖拽和缩放 | Vue和React | 鼠标事件 |
下面仅介绍Vue生态下几个常用的拖拽库:
5.1 Vue3 Dnd
Vue3 DnD
是一个专门为 Vue3
设计的拖放解决方案,它基于 React DnD
的核心程序实现,提供了一种数据驱动的方式来实现拖拽功能,侧重于逻辑处理,允许开发者基于数据灵活地进行定制。
以下是一个简单的示例,展示如何使用 Vue3 DnD
实现拖拽和放置功能:
<template>
<div>
<div v-draggable="draggableOptions" :class="{ draggable: isDragging }" @dragstart="handleDragStart" @dragend="handleDragEnd">
Drag me!
</div>
<div v-droppable="droppableOptions" @drop="handleDrop" @dragover.prevent>
Drop here!
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useDraggable, useDroppable } from '@vueuse/dnd';
const isDragging = ref(false);
const draggableOptions = {
type: 'item',
data: { id: 1, text: 'Drag me!' },
};
const handleDragStart = () => {
isDragging.value = true;
};
const handleDragEnd = () => {
isDragging.value = false;
};
const droppableOptions = {
types: ['item'],
};
const handleDrop = (event) => {
const data = event.dataTransfer.getData('text/plain');
console.log('Dropped item:', data);
};
</script>
5.2 Vue Draggable
vue-draggable
是一个基于 Sortable.js
的 Vue
组件库,用于实现列表项的拖拽排序功能。它支持拖拽、交换位置、复制、移动到其他列表等多种操作,非常适合用于任务管理、列表排序等场景。其实现原理如下:
- 拖拽逻辑:内部使用
Sortable.js
来处理拖拽逻辑,该库封装了 HTML5
的 Drag and Drop API - 事件监听:支持
Sortable.js
的所有事件,触发时会以相同的参数调用 - 状态管理:使用
Vue
的响应式数据绑定机制来跟踪列表项的状态;在拖拽过程中,实时更新数据模型并重新渲染组件 DOM
操作:通过修改元素的 CSS
样式来实现拖拽效果,使用 Sortable.js
提供的 API 来处理拖拽过程中的 DOM
操作- 动画效果:通过设置
animation
属性,可以为拖拽操作添加动画效果,与 Vue
的过渡动画兼容
Vue Draggable
提供了许多配置选项,以下是一些常用的配置项:
配置项 | 说明 |
---|
list | 要拖拽的数组 |
tag | 指定 draggable 组件的根元素类型,默认为 'div' |
clone | 用于克隆被拖拽的元素 |
move | 用于控制元素的移动逻辑 |
componentData | 用于向通过 tag 属性声明的子组件传递额外信息 |
以下是一个简单的示例,展示如何使用 vue-draggable
创建一个可拖动的列表:
<template>
<draggable v-model="list" @end="onEnd">
<div v-for="item in list" :key="list.id">{{ list.name }}</div>
</draggable>
</template>
<script>
import draggable from 'vuedraggable';
export default {
components: {
draggable,
},
data() {
return {
list: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
],
};
},
methods: {
onEnd(event) {
console.log('Drag ended', event);
},
},
};
</script>
5.3 Vue Draggable Resizable
vue-draggable-resizable
允许用户通过拖拽和调整大小来移动和改变元素的尺寸,非常适合需要在页面上自由拖拽和调整元素大小的场景。其主要特性如下:
- 灵活的拖拽和调整大小功能:允许用户自由拖拽元素,并在四个角和边上进行调整大小操作。
- 丰富的配置选项:提供了多种配置项,满足不同场景下的需求。
- 事件驱动:通过事件监听,可以在拖拽和调整大小的过程中执行自定义逻辑。
- 良好的兼容性:与 Vue.js 框架无缝集成,适用于 Vue 2 和 Vue 3
下面是一个使用 vue-draggable-resizable
的示例,展示如何创建一个可拖拽且可调整大小的元素:
<template>
<div id="box">
<vue-draggable-resizable
:w="width"
:h="height"
:x="x"
:y="y"
@dragging="onDrag"
@resizing="onResize"
@resized="onResized"
>
Drag me!
</vue-draggable-resizable>
</div>
</template>
<script>
import VueDraggableResizable from 'vue-draggable-resizable'
import 'vue-draggable-resizable/dist/VueDraggableResizable.css'
export default {
components: {
VueDraggableResizable
},
data() {
return {
x: 0,
y: 0,
width: 100,
height: 100
}
},
methods: {
// 拖拽过程中更新组件位置
onDrag(x, y) {
this.x = x
this.y = y
},
// 调整大小过程中更新组件尺寸
onResize(x, y, width, height) {
this.x = x
this.y = y
this.width = width
this.height = height
},
// 调整大小结束时的回调
onResized() {
console.log('Resized')
}
}
}
</script>
6. 可视化大屏编辑器中的拖拽
可视化大屏编辑器允许用户通过拖拽添加到画布,对一个组件来说,一个完整的拖拽流程由两部分组成:
- 拖拽组件放置到画布;
- 在画布中拖拽组件调整位置、大小等信息。

6.1 组件拖拽到画布
拖拽组件放置到画布本质是将拖动源携带的数据传到画布制定区域,目标源监听事件拿到携带的数据动态渲染出实际的组件。过程如下:
实现方面可以直接使用HTML5
原生的拖放API
组件列表的伪代码:
<div v-for="item in componentList" :key="item" class="list"
draggable="true" :data-type="item" @dragstart="handleDragStart">
<div>
<span class="iconfont" :class="'icon-' + componentIconMap[item]"></span>
</div>
</div>
handleDragStart(e) {
e.dataTransfer.setData('type', e.target.dataset.type)
},
给列表中的每一个组件都设置了 draggable
属性。另外,在触发 dragstart
事件时,使用 dataTransfer.setData()
传输数据。
接收数据的代码如下:
<div id="main-container" @drop="handleDrop" @dragover="handleDragover">
</div>
handleDrop(e) {
e.preventDefault()
e.stopPropagation()
const componentName = e.dataTransfer.getData('type');
const rectInfo = $('#canvas-container')
.getBoundingClientRect();
const left = e.x - rectInfo.left;
const top = e.y - rectInfo.top;
const componentInfo = {
id: generateID(),
component: componentName,
style: {
left,
top
}
}
this.addComponent(componentInfo)
},
触发 drop
事件时,使用 dataTransfer.getData()
接收传输过来的数据,然后根据找到对应的组件数据,再添加到画布,从而渲染组件。
6.2 组件在画布中移动
实现拖拽和调整元素大小的功能可以通过监听鼠标事件来控制元素的位置和尺寸变化。伪代码如下:
element.onmousedown = (event) => {
}
element.onmousemove = (event) => {
}
element.onmouseup = (event) => {
}
这种方式相对灵活,可以根据具体需求进行定制,不局限于拖拽和放置这种固定操作模式,比如直接拖动改变位置、双击改变大小等。但需要更多的逻辑处理来确保拖拽的准确性和流畅性。实际开发中,通常使用第三方库实现,如vue-draggable-resizable
,movable
等,适用于Vue
的主流拖拽库对比如下:
实现方式 | 基础拖拽能力 | 使用场景 |
---|
原生鼠标事件 | 移动、缩放、旋转 | 简单的拖拽功能 |
vue draggable | 拖放、分组拖拽 | 对列表或网格中的元素进行排序 |
vue-drag-resize | 移动、缩放、旋转 | 实现可拖拽且可调整大小的组件 |
movable | 移动、缩放、旋转 | 简单的拖拽移动 |
以下是 vue-drag-resize 的使用示例:
<template>
<div id="box">
<vue-draggable-resizable :w="width" :h="height" :x="x" :y="y" @dragging="onDrag" @resizing="onResize">
Drag me!
</vue-draggable-resizable>
</div>
</template>
<script>
import VueDraggableResizable from 'vue-draggable-resizable';
import 'vue-draggable-resizable/dist/VueDraggableResizable.css';
export default {
components: {
VueDraggableResizable
},
data() {
return {
x: 0,
y: 0,
width: 100,
height: 100
};
},
methods: {
// 更新组件位置
onDrag(x, y) {
this.x = x;
this.y = y;
},
// 更新组件尺寸
onResize(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
}
};
</script>
7. 总结
前端拖拽技术作为一种强大的交互模式,已经成为现代 Web 应用的标配。无论是实现简单的元素拖放,还是构建复杂的可视化编辑器,掌握拖拽技术有助于我们构建更加直观、高效的用户界面,为用户提供更优质的体验。希望本文能够帮助读者全面了解前端拖拽技术,并在实际项目中灵活应用。
转自https://juejin.cn/post/7491164546045624356
该文章在 2025/4/12 10:48:20 编辑过