Vue3 自定义指令完全指南
Vue3 自定义指令完全指南
概述
Vue3 不仅支持官方内置指令(v-if、v-for、v-model 等),还允许开发者注册自定义指令实现对 DOM 的底层操作。自定义指令的签名与组件类似,拥有完整的生命周期钩子,适用于聚焦输入框、权限控制、拖拽、图片懒加载等通用 DOM 操作场景。
为什么需要自定义指令
在 Vue 项目中,某些 DOM 操作逻辑会在多个组件中重复出现:输入框自动聚焦、权限按钮显隐、滚动监听、IntersectionObserver 懒加载等。如果每个组件都写一遍,会造成大量重复代码。自定义指令将这类逻辑封装在一起,一次定义,全局或局部复用。
基础语法
// 方式1:局部指令(仅当前组件可用)
const vFocus = {
mounted: (el) => el.focus()
}
// 方式2:全局指令(整个应用可用)
const vPermission = {
mounted(el, binding) {
// el: 指令绑定的 DOM 元素
// binding.value: 指令的绑定值(v-permission="admin" 中的 "admin")
// binding.arg: 指令参数(v-permission:action 中的 "action")
// binding.modifiers: 修饰符对象
}
}
app.directive('focus', vFocus)
app.directive('permission', vPermission)
// 简写:mounted + updated 相同逻辑可用函数简写
app.directive('color', (el, binding) => {
el.style.color = binding.value
})
完整代码示例
// ========== main.js - 全局指令注册 ==========
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// —— 全局指令 1:v-focus 自动聚焦 ——
app.directive('focus', {
mounted(el) {
el.focus()
}
})
// —— 全局指令 2:v-permission 权限控制 ——
app.directive('permission', {
mounted(el, binding) {
// binding.value: 需要的权限,如 'admin'、'editor'
// 从全局 store 或 inject 获取权限列表
const userPermissions = getCurrentPermissions() // 模拟获取
if (!userPermissions.includes(binding.value)) {
// 无权限:移除元素或禁用
if (typeof binding.modifiers.disable === 'undefined') {
el.remove()
} else {
el.disabled = true
el.style.opacity = '0.5'
el.style.pointerEvents = 'none'
}
}
},
// 当组件更新(权限可能变化)时重新检查
updated(el, binding) {
if (binding.value !== binding.oldValue) {
const userPermissions = getCurrentPermissions()
if (!userPermissions.includes(binding.value)) {
el.remove()
}
}
}
})
// —— 全局指令 3:v-lazyload 图片懒加载 ——
app.directive('lazyload', {
mounted(el, binding) {
if (!('IntersectionObserver' in window)) {
// 不支持 IntersectionObserver 的浏览器直接加载
el.src = binding.value
return
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
el.src = binding.value
observer.unobserve(el) // 加载后停止观察
}
})
},
{ rootMargin: '50px' } // 提前 50px 开始加载
)
observer.observe(el)
// 缓存 observer 用于 unmounted 时清理
el._lazyObserver = observer
},
unmounted(el) {
if (el._lazyObserver) {
el._lazyObserver.disconnect()
}
}
})
// —— 全局指令 4:v-draggable 元素拖拽 ——
app.directive('draggable', {
mounted(el) {
el.style.position = 'absolute'
el.style.cursor = 'move'
el.style.userSelect = 'none'
let isDragging = false
let offsetX = 0
let offsetY = 0
el.addEventListener('mousedown', (e) => {
isDragging = true
offsetX = e.clientX - el.offsetLeft
offsetY = e.clientY - el.offsetTop
el.style.zIndex = '9999'
})
document.addEventListener('mousemove', (e) => {
if (!isDragging) return
el.style.left = (e.clientX - offsetX) + 'px'
el.style.top = (e.clientY - offsetY) + 'px'
})
document.addEventListener('mouseup', () => {
isDragging = false
})
}
})
// —— 全局指令 5:v-click-outside 点击外部 ——
app.directive('click-outside', {
mounted(el, binding) {
el._clickOutside = (event) => {
// 如果点击目标不在 el 内部,执行绑定的方法
if (!el.contains(event.target)) {
binding.value(event)
}
}
document.addEventListener('click', el._clickOutside)
},
unmounted(el) {
document.removeEventListener('click', el._clickOutside)
}
})
// 模拟权限获取(实际项目中从 Pinia/Vuex 获取)
function getCurrentPermissions() {
return ['read', 'write', 'admin'] // 模拟有 admin 权限
}
app.mount('#app')
// ========== UserPanel.vue - 使用自定义指令 ==========
<script setup>
import { ref } from 'vue'
const isDropdownOpen = ref(false)
const username = ref('林小鸣')
function toggleDropdown() {
isDropdownOpen.value = !isDropdownOpen.value
}
function closeDropdown() {
isDropdownOpen.value = false
}
</script>
<template>
<div class="user-panel">
<!-- v-focus:页面加载后自动聚焦到搜索框 -->
<input
v-focus
v-model="username"
placeholder="输入用户名..."
class="username-input"
/>
<!-- v-permission:没有 editor 权限时,该按钮不渲染 -->
<button v-permission="'editor'" class="btn-edit">
编辑资料
</button>
<!-- v-permission:admin 传入参数,只有 admin 权限可见 -->
<button v-permission:admin class="btn-delete">
删除用户
</button>
<!-- v-permission 带 disable 修饰符,无权限时按钮禁用而非移除 -->
<button v-permission="'superuser'" class="btn-other" v-permission.disable>
超级用户功能
</button>
<!-- v-click-outside:点击下拉菜单外部自动关闭 -->
<div class="dropdown" v-click-outside="closeDropdown">
<button @click="toggleDropdown">
{{ isDropdownOpen ? '关闭' : '打开' }}下拉菜单
</button>
<div v-if="isDropdownOpen" class="dropdown-menu">
<a href="#">菜单项1</a>
<a href="#">菜单项2</a>
</div>
</div>
</div>
</template>
<style scoped>
.user-panel { padding: 1rem; display: flex; flex-direction: column; gap: 0.75rem; }
.username-input { padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; }
button { padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; }
.btn-edit { background: #42b983; color: white; }
.btn-delete { background: #e74c3c; color: white; }
.btn-other { background: #999; color: white; }
.dropdown { position: relative; display: inline-block; }
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 150px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.dropdown-menu a { display: block; padding: 0.5rem; text-decoration: none; color: #333; }
.dropdown-menu a:hover { background: #f5f5f5; }
</style>
运行效果说明
页面加载后搜索框自动获得焦点(v-focus)。拥有 admin 权限时"删除用户"按钮正常显示,无权限时按钮不渲染。"编辑资料"按钮显示(有 editor 权限),"超级用户功能"按钮禁用状态(有 superuser 权限则正常启用)。点击"打开下拉菜单"展开菜单,点击菜单外部区域菜单自动收起(v-click-outside)。
常见问题
Q1:自定义指令的钩子函数有哪些?完整生命周期是什么?
完整钩子:created(元素属性/事件绑定前)、beforeMount(首次绑定但未插入 DOM)、mounted(插入 DOM 后)、beforeUpdate(组件更新前)、updated(组件更新后)、beforeUnmount(卸载前)、unmounted(卸载后)。大多数场景只需 mounted 和 updated 即可。
Q2:自定义指令如何传递多个参数或复杂值?
v-permission="['admin', 'editor']" 可以传递数组。v-format:date="'YYYY-MM-DD'" 可以传递参数和值。binding 对象包含:value(绑定值)、arg(参数,如 'date')、modifiers(修饰符对象,如 { capitalize: true })。
Q3:局部指令和全局指令如何选择?
只在单个组件使用的指令用局部注册(const vFocus = {...})。在多个组件共用的指令(聚焦、权限、懒加载等)用全局注册。全局指令在 main.js 中注册一次,任意组件模板中直接使用,无需 import。
延伸阅读
- <a href="#">Vue3 组合式 API 完全指南</a> —— 组合式 API 与项目结构
- <a href="#">Vue3 生命周期钩子详解</a> —— mounted/unmounted 处理 DOM 清理逻辑
- <a href="#">Vue3 依赖注入 provide/inject 详解</a> —— 与自定义指令配合实现权限控制