Directive指令

0 阅读4分钟

Directive指令: 就是给某个 DOM(或组件的根元素)附加“行为”的语法,用法形态是 v-xxx。 当我们不想为了一个小行为专门封装成组件时,就用指令更合适。

什么时候用指令最合适

  • 直接操作 DOM 的行为(组件反而更麻烦)
  • 自动聚焦:v-focus
  • 点击外部关闭:v-click-outside
  • 拖拽:v-drag
  • 复制到剪贴板:v-copy
  • 权限/可见性控制
  • v-permission="'user:add'" 没权限就隐藏/禁用按钮
  • 事件增强
  • 点击防抖/节流:v-debounce / v-throttle
  • 输入/展示增强
  • 数字格式化、只允许输入数字、自动滚动到底部等
和组件的区别(最关键)
  • 组件:复用一块 UI(结构+样式+逻辑)
  • 指令:复用一个“行为/规则”,附加到已有元素上
一个最小例子:v-focus

注册(全局):

// main.ts
app.directive('focus', {
  mounted(el: HTMLElement) {
    el.focus()
  },
})

使用:

<template>
  <input v-focus />
</template>

指令一般在什么时候触发

可以把它理解成“生命周期钩子”,常用的是:

  • mounted(el):元素插入页面后(最常用)
  • updated(el):组件更新后
  • beforeUnmount(el):卸载前清理事件/定时器

什么时候需要“全局注册指令”

当一个指令满足下面任意一条,就很适合全局注册(app.directive(...)):

  • 全项目很多地方都会用:例如 v-permission(权限)、v-focus(自动聚焦)、v-copy(复制)、v-click-outside(点外关闭)
  • 属于“通用行为能力”:不依赖某个具体页面的业务数据,只依赖传入参数/当前元素
  • 需要统一标准:比如所有按钮权限判断都走同一个指令,避免每个页面自己写一套 if 判断

不建议全局注册的情况:

  • 只在某一个页面/组件用一次:局部注册或直接写在组件里更清晰

  • 强业务绑定:例如只对“订单模块”有效的行为,放在该模块内部更好,避免污染全局命名

在 main.ts 里怎么放:专门注册还是混在逻辑里?

推荐把 main.ts 保持“干净”,只负责创建 app + 安装各种初始化;指令通常集中放在一个注册函数/文件里,然后在 main.ts 调一下。 常见结构是这种思路:

  • src/directives/index.ts:统一注册所有全局指令(一个入口)
  • src/directives/*.ts:每个指令一个文件(便于维护)
  • main.ts:只做 setupDirectives(app) 这种“安装动作” main.ts 里就像装插件一样装它(概念上它就是一段“应用初始化逻辑”)。

“注册指令”本质是什么逻辑

本质就是:把一个名字(例如 focus)和一段 DOM 行为实现绑定到 app 上,之后模板里写 v-focus 时,Vue 就能找到这段实现并在合适的时机(如 mounted)执行。

思路:

  • 写指令:一个指令一个文件(只实现行为),放在 src/directives/xxx.ts
  • 统一注册:做一个 src/directives/index.ts 导出 setupDirectives(app),把所有指令都 app.directive(name, impl) 注册进去
  • 入口安装:在 main.ts 里 setupDirectives(app),就完成“全局可用”

image.png

实现:

  1. 指令实现:v-focus(自动聚焦)
//focus.ts
import type { Directive } from 'vue'

/**
 * v-focus
 * Auto focus an element when mounted.
 *
 * Usage:
 *  <input v-focus />
 */
export const vFocus: Directive<HTMLElement> = {
  mounted(el) {
    // next tick not strictly required; mounted means it's in DOM.
    el.focus?.()
  },
}
  1. 指令实现:v-permission(权限隐藏/禁用)
//permission.ts
import type { Directive, DirectiveBinding } from 'vue'
import { useUserStore } from '../stores/user'

type PermissionValue = string | string[]

function normalize(value: unknown): string[] {
  if (typeof value === 'string') return [value]
  if (Array.isArray(value) && value.every((x) => typeof x === 'string')) return value as string[]
  return []
}

function hasAnyPermission(required: string[], owned: string[]) {
  if (required.length === 0) return true
  if (owned.includes('*')) return true
  return required.some((p) => owned.includes(p))
}

function applyPermission(el: HTMLElement, binding: DirectiveBinding<PermissionValue>) {
  const user = useUserStore()
  const required = normalize(binding.value)
  const owned = user.permissions ?? []

  const ok = hasAnyPermission(required, owned)

  if (ok) {
    // restore if previously disabled/hidden by this directive
    if ((el as any).__v_permission_display !== undefined) {
      el.style.display = (el as any).__v_permission_display as string
      delete (el as any).__v_permission_display
    }
    if ((el as any).__v_permission_disabled !== undefined) {
      ;(el as HTMLButtonElement).disabled = (el as any).__v_permission_disabled as boolean
      delete (el as any).__v_permission_disabled
    }
    return
  }

  // Strategy:
  // - if using modifiers: v-permission.disable => disable the element
  // - otherwise: hide it
  if (binding.modifiers.disable) {
    ;(el as any).__v_permission_disabled = (el as HTMLButtonElement).disabled
    ;(el as HTMLButtonElement).disabled = true
  } else {
    ;(el as any).__v_permission_display = el.style.display
    el.style.display = 'none'
  }
}

export const vPermission: Directive<HTMLElement, PermissionValue> = {
  mounted(el, binding) {
    applyPermission(el, binding)
  },
  updated(el, binding) {
    applyPermission(el, binding)
  },
}
  1. 统一注册入口:setupDirectives(app)
//index.ts
import type { App } from 'vue'
import { vFocus } from './focus'
import { vPermission } from './permission'

export function setupDirectives(app: App) {
  app.directive('focus', vFocus)
  app.directive('permission', vPermission)
}
  1. 在 main.ts 安装(全局注册生效)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

import App from './App.vue'
import './style.css'
import { setupDirectives } from './directives'

const app = createApp(App)

const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(ElementPlus)
setupDirectives(app)

app.mount('#app')

在页面里怎么用

  • 聚焦:<input v-focus />
  • 权限隐藏:<el-button v-permission="'user:add'">新增</el-button>
  • 权限禁用(加 modifier):<el-button v-permission.disable="'user:add'">新增</el-button>

可以的——只要你在 main.ts 里做了全局注册(你项目里是 setupDirectives(app)),那这些指令就对整个应用的所有组件/页面都可用。

但有 2 个前提/边界你要知道

  • 前提 1:必须被注册过

  • 例如你现在注册了 focus 和 permission,所以任何页面都能写 v-focus、v-permission。

  • 前提 2:只对“当前这个 app 实例”有效

  • 如果你页面里还有另一个 createApp() 创建出来的独立应用(少见),它不会自动继承这些指令,需要在那个 app 里也注册一遍。

实际使用上你会遇到的限制

  • 指令最终是作用在一个元素(el)上,所以它能不能生效取决于你绑在什么上:
  • v-focus:要绑在能 focus() 的元素上(input/textarea/可聚焦元素)
  • v-permission:绑在按钮/容器都行,没权限会隐藏或禁用(你用 .disable 修饰符就是禁用)