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),就完成“全局可用”
实现:
- 指令实现: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?.()
},
}
- 指令实现: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)
},
}
- 统一注册入口: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)
}
- 在 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 修饰符就是禁用)