Vue3 自定义指令完全指南

小飞兽 Vue.js 349 次阅读 2026-06-20

Vue3 自定义指令完全指南

概述

Vue3 不仅支持官方内置指令(v-ifv-forv-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(卸载后)。大多数场景只需 mountedupdated 即可。

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> —— 与自定义指令配合实现权限控制