鱼泡网免费下载
104.91MB · 2025-10-13
在前端开发中,元素拖动是常见交互需求(如可拖拽弹窗、自定义布局组件、拖拽排序模块等)。原生实现拖动需处理多端事件(mousedown/mousemove/mouseup)、计算坐标偏移、限制边界、优化性能等,重复编码成本高且易出现兼容性或性能问题。
为解决上述痛点,封装 Vue 指令 draggable,将拖动逻辑抽象为可复用的指令,避免重复开发,同时通过性能优化确保拖动流畅性,降低业务代码与拖动逻辑的耦合度。
该指令用于为 Vue 组件或 DOM 元素快速添加拖动功能,支持以下核心场景:
指令基于 Vue 3 指令生命周期(mounted/unmounted)设计,核心逻辑围绕「事件监听 - 状态管理 - 坐标计算 - 性能优化」展开,具体拆解如下:
mounted 是指令绑定到 DOM 元素后的初始化入口,主要完成 5 件事:
拖动逻辑依赖「鼠标按下 - 移动 - 释放」三步事件,函数职责如下:
函数名 | 触发事件 | 核心逻辑 |
---|---|---|
handleMouseDown | mousedown | 1. 仅响应鼠标左键(e.button === 0);2. 计算 offsetX/offsetY;3. 提升元素 z-index 避免遮挡;4. 阻止句柄文本选中(e.preventDefault)。 |
handleMouseMove | mousemove | 1. 若未处于拖动状态(!isDragging)则跳过;2. 计算鼠标当前坐标与偏移量的差值(目标位置);3. 调用 updatePosition 更新元素位置。 |
handleMouseUp | mouseup/mouseleave | 1. 重置 isDragging 为 false;2. 取消未执行的 requestAnimationFrame;3. (可选)恢复 z-index(当前代码注释保留,按需启用)。 |
updatePosition | 内部调用 | 1. 用 requestAnimationFrame 确保动画流畅(与浏览器重绘节奏同步);2. 计算视口与元素尺寸,限制元素不超出视口;3. 设置元素 left/top 完成定位。 |
当绑定指令的元素被销毁时,需清理资源防止内存泄漏:
/**
* Vue 可拖动指令(draggable)
* 功能:支持元素拖动、指定拖动句柄、限制视口边界、性能优化
* 适用场景:可拖拽弹窗、自定义布局组件、轻量拖动交互
*/
export const draggable = {
/**
* 指令挂载到元素时执行(初始化逻辑)
* @param {HTMLElement} el - 绑定指令的DOM元素
* @param {Object} binding - 指令绑定信息(value为拖动句柄选择器)
*/
mounted(el, binding) {
// 1. 确定拖动句柄(默认整个元素,支持通过binding.value指定选择器)
let dragHandle = el;
if (binding.value) {
const handle = el.querySelector(binding.value);
if (handle) dragHandle = handle;
}
// 2. 初始化样式(性能优化+定位)
el.style.willChange = 'left, top'; // 告知浏览器提前优化动画
if (!el.style.position || el.style.position === 'static') {
el.style.position = 'fixed'; // 确保拖动基于视口定位
}
// 3. 核心状态变量
let isDragging = false; // 拖动状态标记
let offsetX = 0; // 鼠标与元素左边界的偏移量
let offsetY = 0; // 鼠标与元素上边界的偏移量
let animationId = null; // requestAnimationFrame ID(用于取消动画)
// 缓存元素尺寸(避免mousemove时重复计算)
const getElementSize = () => ({
width: el.offsetWidth,
height: el.offsetHeight
});
/**
* 4. 事件处理函数:鼠标按下(触发拖动开始)
* @param {MouseEvent} e - 鼠标事件对象
*/
const handleMouseDown = (e) => {
if (e.button !== 0) return; // 仅允许鼠标左键拖动
isDragging = true;
// 计算偏移量:鼠标坐标 - 元素边界坐标(避免拖动时元素跳变)
const elementRect = el.getBoundingClientRect();
offsetX = e.clientX - elementRect.left;
offsetY = e.clientY - elementRect.top;
el.style.zIndex = 1000; // 提升层级,避免被其他元素遮挡
if (e.target === dragHandle) e.preventDefault(); // 防止句柄文本被选中
};
/**
* 事件处理函数:更新元素位置(性能优化:requestAnimationFrame)
* @param {number} x - 目标X坐标
* @param {number} y - 目标Y坐标
*/
const updatePosition = (x, y) => {
if (animationId) cancelAnimationFrame(animationId); // 取消未执行的动画
animationId = requestAnimationFrame(() => {
const { width: elementWidth, height: elementHeight } = getElementSize();
const viewportWidth = window.innerWidth; // 视口宽度
const viewportHeight = window.innerHeight; // 视口高度
// 计算边界:确保元素完全在视口内
const minX = 0;
const maxX = viewportWidth - elementWidth;
const minY = 0;
const maxY = viewportHeight - elementHeight;
// 限制坐标在边界内
const constrainedX = Math.max(minX, Math.min(x, maxX));
const constrainedY = Math.max(minY, Math.min(y, maxY));
// 应用最终位置
el.style.left = `${constrainedX}px`;
el.style.top = `${constrainedY}px`;
});
};
/**
* 事件处理函数:鼠标移动(执行拖动逻辑)
* @param {MouseEvent} e - 鼠标事件对象
*/
const handleMouseMove = (e) => {
if (!isDragging) return; // 非拖动状态跳过
// 计算目标坐标:鼠标坐标 - 偏移量
const targetX = e.clientX - offsetX;
const targetY = e.clientY - offsetY;
updatePosition(targetX, targetY);
};
/**
* 事件处理函数:鼠标释放/离开(结束拖动)
*/
const handleMouseUp = () => {
isDragging = false;
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
// 可选:恢复z-index(根据业务需求启用)
// el.style.zIndex = '';
};
// 5. 绑定事件监听
dragHandle.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mouseleave', handleMouseUp); // 处理鼠标移出浏览器
// 保存 handlers 供卸载时清理
el._draggableHandlers = {
handleMouseDown,
handleMouseMove,
handleMouseUp,
dragHandle
};
},
/**
* 指令卸载时执行(清理资源)
* @param {HTMLElement} el - 绑定指令的DOM元素
*/
unmounted(el) {
const handlers = el._draggableHandlers;
if (handlers) {
// 移除所有事件监听
handlers.dragHandle.removeEventListener('mousedown', handlers.handleMouseDown);
document.removeEventListener('mousemove', handlers.handleMouseMove);
document.removeEventListener('mouseup', handlers.handleMouseUp);
document.removeEventListener('mouseleave', handlers.handleMouseUp);
// 清理引用,防止内存泄漏
delete el._draggableHandlers;
}
}
};
首先需在 Vue 项目中注册指令,支持全局注册或局部注册。
import { createApp } from 'vue';
import App from './App.vue';
import { draggable } from './directives/draggable'; // 导入指令
const app = createApp(App);
app.directive('draggable', draggable); // 全局注册指令
app.mount('#app');
<template>
<!-- 组件模板 -->
</template>
<script setup>
import { draggable } from './directives/draggable'; // 导入指令
</script>
适用于简单元素(如小面板、卡片),整个元素均可触发拖动。
<template>
<div
v-draggable
class="draggable-card"
>
<p>整个卡片可拖动</p>
</div>
</template>
<style scoped>
.draggable-card {
width: 300px;
height: 200px;
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 16px;
cursor: move; /* 提示可拖动 */
}
</style>
适用于弹窗、复杂组件(仅允许特定区域拖动,如标题栏)。
<template>
<!-- 弹窗组件:仅标题栏(.modal-header)可拖动 -->
<div v-draggable=".modal-header" class="modal">
<!-- 拖动句柄 -->
<div class="modal-header">
<h3>弹窗标题(可拖动)</h3>
</div>
<!-- 不可拖动区域 -->
<div class="modal-content">
<p>弹窗内容,点击此处不可拖动</p>
<button @click="closeModal">关闭</button>
</div>
</div>
</template>
<style scoped>
.modal {
width: 400px;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.2);
}
.modal-header {
padding: 12px 16px;
background: #f5f5f5;
border-bottom: 1px solid #eee;
cursor: move; /* 提示可拖动 */
}
.modal-content {
padding: 16px;
cursor: default; /* 提示不可拖动 */
}
</style>