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 編輯過