Vue 3 自定义指令实战:实现可复用的 DOM 拖拽功能
Vue 3 自定义指令封装 DOM 拖拽功能
在 Vue 3 开发中,对于跨组件复用的纯逻辑函数,通常将其提取到独立的 TS/JS 模块中。然而,当需求涉及直接操作 DOM 元素(如添加拖拽、聚焦、权限控制等行为)时,使用自定义指令(Custom Directives)是更优雅且符合 Vue 数据驱动理念的方案。
本文将以实现一个通用的 DOM 拖拽指令为例,讲解如何注册自定义指令,并探讨在原生 HTML 元素与第三方 UI 组件库中的不同应用策略。
1. 全局注册拖拽指令
在应用入口文件(如 main.ts)中全局注册指令。为了保证事件绑定的安全性,推荐使用 addEventListener 替代直接赋值,并增加对目标元素定位属性的自动处理。
import { createApp, DirectiveBinding } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.directive('draggable', {
mounted(element: HTMLElement, binding: DirectiveBinding) {
// 支持通过 binding.value 传入目标元素的选择器,默认为当前元素
const targetSelector = binding.value;
const targetEl = targetSelector
? document.querySelector(targetSelector) as HTMLElement
: element;
if (!targetEl) return;
// 确保目标元素具备定位属性
const position = window.getComputedStyle(targetEl).position;
if (position === 'static') {
targetEl.style.position = 'relative';
}
let startX = 0;
let startY = 0;
let initialLeft = 0;
let initialTop = 0;
const handleMouseMove = (event: MouseEvent) => {
const deltaX = event.clientX - startX;
const deltaY = event.clientY - startY;
targetEl.style.left = `${initialLeft + deltaX}px`;
targetEl.style.top = `${initialTop + deltaY}px`;
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
const handleMouseDown = (event: MouseEvent) => {
event.preventDefault();
startX = event.clientX;
startY = event.clientY;
initialLeft = targetEl.offsetLeft;
initialTop = targetEl.offsetTop;
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
// 将触发事件绑定在指令所在的元素上(作为拖拽手柄)
element.addEventListener('mousedown', handleMouseDown);
}
});
app.mount('#app');
2. 在基础 HTML 元素中使用
对于普通的 DOM 节点,只需在元素上添加 v-draggable 指令即可使其具备拖拽能力。
<template>
<!-- 默认拖拽自身 -->
<div class="drag-box" v-draggable>
拖拽此区域
</div>
<!-- 图片元素同样适用 -->
<img
src="/logo.png"
alt="Logo"
class="drag-image"
v-draggable
/>
</template>
<style scoped>
.drag-box {
width: 150px;
height: 150px;
background-color: #409eff;
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: move;
}
.drag-image {
width: 100px;
cursor: move;
}
</style>
3. 适配第三方 UI 组件库
在使用 Ant Design Vue 或 Element Plus 等 UI 组件库时,组件内部通常存在多层 DOM 嵌套。如果直接对组件内部的某个子元素使用指令,可能会导致拖拽错位。此时,可以利用指令的 binding.value 传递 CSS 选择器,将"拖拽手柄"与"实际移动的目标容器"分离。
以下以 Ant Design Vue 的 Modal 弹窗为例,将弹窗的标题栏作为拖拽手柄,移动整个弹窗内容区:
<template>
<a-button type="primary" @click="isVisible = true">
打开可拖拽弹窗
</a-button>
<a-modal
v-model:open="isVisible"
:footer="null"
:closable="false"
class="draggable-modal"
>
<!-- 使用插槽自定义标题,并传入弹窗内容区的类名作为拖拽目标 -->
<template #title>
<div v-draggable="'.ant-modal-content'" style="cursor: move;">
按住此处拖拽弹窗
</div>
</template>
<p>这是弹窗的主体内容。</p>
<p>通过自定义指令,我们轻松实现了弹窗的拖拽交互。</p>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const isVisible = ref(false);
</script>