自定义指令

当内置指令无法满足需求时,你可以编写自定义指令。自定义指令适合封装直接操作 DOM 的逻辑,例如自动聚焦、权限控制、拖拽、懒加载等。

定义指令

使用 defineDirective 定义一个指令,需要提供指令名称和指令对象:

tsx
import { defineDirective } from 'vitarx'

defineDirective('focus', {
  mounted(el, binding) {
    if (binding.value) {
      el.focus()
    }
  }
})
  • 第一个参数是指令名称,在 JSX 中使用时需要加上 v- 前缀(如 v-focus
  • 第二个参数是指令对象,包含生命周期钩子函数

指令钩子

指令对象可以包含以下钩子函数:

钩子调用时机用途
created元素已创建时初始化状态、设置副作用
mounted元素挂载到 DOM 后操作 DOM、绑定事件
dispose元素即将被销毁清理副作用、解绑事件
getSSRProps服务端渲染时返回需要合并的属性对象

每个钩子函数接收三个参数:

typescript
(el: HostElement, binding: DirectiveBinding, view: ElementView) => void
  • el:指令绑定的 DOM 元素
  • binding:指令绑定信息,包含 value(指令绑定的值)和可选的 arg(指令参数)
  • view:视图节点实例

DirectiveBinding

typescript
interface DirectiveBinding {
  /** 指令绑定的值 */
  readonly value: any
  /** 指令绑定的参数 */
  readonly arg?: string
}

在 JSX 中使用

定义好指令后,可以直接在 JSX 中通过 v-指令名 使用:

tsx
import { ref, createApp, defineDirective } from 'vitarx'

// 定义自动聚焦指令
defineDirective('focus', {
  mounted(el, binding) {
    if (binding.value) {
      el.focus()
    }
  }
})

function App() {
  const shouldFocus = ref(true)

  return <input v-focus={shouldFocus} placeholder="自动聚焦" />
}

createApp(App).mount('#app')

withDirectives — 手动绑定指令

除了在 JSX 中使用 v- 前缀语法,你还可以使用 withDirectives 函数手动为视图绑定指令。这在需要动态绑定指令的场景下很有用。

tsx
import { defineDirective, withDirectives, ref, createApp } from 'vitarx'

// 定义指令
defineDirective('color', {
  mounted(el, binding) {
    el.style.color = binding.value
  }
})

function App() {
  const color = ref('red')

  // 使用 withDirectives 手动绑定指令
  return withDirectives(<div>彩色文字</div>, [['color', { value: color }]])
}

createApp(App).mount('#app')

withDirectives 接受两个参数:

  • view:要添加指令的视图节点
  • directives:指令数组,每个元素是一个元组 [name, binding]
    • name 可以是字符串(指令名称)或指令对象本身
    • bindingDirectiveBinding 对象,包含 value 和可选的 arg

指令注册方式

指令有三种注册方式,对应不同的作用范围:

1. 全局注册

在组件外部调用 defineDirective,指令会被注册到全局缓存中,所有组件都可以使用:

tsx
import { defineDirective } from 'vitarx'

// 全局注册,所有组件可用
defineDirective('focus', {
  mounted(el, binding) {
    if (binding.value) el.focus()
  }
})

2. 组件局部注册

在组件函数内部调用 defineDirective,指令只会注册到当前组件的作用域中:

tsx
import { defineDirective, ref, createApp } from 'vitarx'

function App() {
  // 组件局部注册,仅当前组件可用
  defineDirective('highlight', {
    mounted(el, binding) {
      el.style.backgroundColor = binding.value
    }
  })

  const color = ref('yellow')

  return <span v-highlight={color}>高亮文本</span>
}

createApp(App).mount('#app')

3. 应用级注册

通过 app.directive() 注册指令,指令在该应用实例的范围内可用:

tsx
import { createApp } from 'vitarx'

const app = createApp(App)

// 应用级注册,该应用下所有组件可用
app.directive('focus', {
  mounted(el, binding) {
    if (binding.value) el.focus()
  }
})

app.mount('#app')

app.directive() 也可以用来获取已注册的指令:

tsx
// 获取已注册的指令
const focusDirective = app.directive('focus')

指令查找优先级

当多个作用域中存在同名指令时,按以下优先级查找:

  1. 组件局部指令 — 在组件内部通过 defineDirective 注册的
  2. 应用级指令 — 通过 app.directive() 注册的
  3. 全局指令 — 在组件外部通过 defineDirective 注册的

resolveDirective — 解析指令

resolveDirective 根据指令名称查找对应的指令对象,查找顺序遵循上述优先级。名称中的 v- 前缀可以省略:

tsx
import { resolveDirective } from 'vitarx'

// 以下两种写法等价
const directive1 = resolveDirective('focus')
const directive2 = resolveDirective('v-focus')

完整示例

自定义 focus 指令

下面是一个完整的自动聚焦指令示例,支持动态控制是否聚焦:

tsx
import { ref, createApp, defineDirective } from 'vitarx'

// 定义自动聚焦指令
defineDirective('focus', {
  mounted(el, binding) {
    // binding.value 控制是否聚焦
    if (binding.value) {
      el.focus()
    }
  }
})

function App() {
  const autoFocus = ref(true)

  return (
    <div style={{ padding: '20px', maxWidth: '400px', margin: '0 auto' }}>
      <h2>自动聚焦指令示例</h2>
      <input v-focus={autoFocus} placeholder="我会自动聚焦" />
      <div style={{ marginTop: '12px' }}>
        <button
          onClick={() => {
            autoFocus.value = true
          }}
        >
          启用聚焦
        </button>
        <button
          onClick={() => {
            autoFocus.value = false
          }}
        >
          关闭聚焦
        </button>
      </div>
    </div>
  )
}

createApp(App).mount('#app')

自定义权限指令

下面是一个权限控制指令的示例,没有权限时移除元素:

tsx
import { ref, createApp, defineDirective, type DirectiveBinding } from 'vitarx'

// 定义权限指令
defineDirective('permission', {
  mounted(el, binding) {
    // binding.value 是当前用户拥有的权限列表
    // binding.arg 是该元素需要的权限标识
    const userPermissions: string[] = binding.value || []
    const requiredPermission = binding.arg

    // 如果用户没有所需权限,移除元素
    if (requiredPermission && !userPermissions.includes(requiredPermission)) {
      el.parentNode?.removeChild(el)
    }
  }
})

function App() {
  // 模拟当前用户的权限列表
  const permissions = ref(['read', 'edit'])

  return (
    <div style={{ padding: '20px', maxWidth: '400px', margin: '0 auto' }}>
      <h2>权限指令示例</h2>
      <p>当前权限:{permissions.value.join(', ')}</p>

      {/* 有 read 权限,显示 */}
      <div v-permission={[permissions.value, 'read']} style={{ color: 'green' }}>
        ✅ 你有查看权限
      </div>

      {/* 有 edit 权限,显示 */}
      <div v-permission={[permissions.value, 'edit']} style={{ color: 'green' }}>
        ✅ 你有编辑权限
      </div>

      {/* 没有 delete 权限,不显示 */}
      <div v-permission={[permissions.value, 'delete']} style={{ color: 'red' }}>
        ❌ 你有删除权限
      </div>

      <div style={{ marginTop: '12px' }}>
        <button
          onClick={() => {
            permissions.value = ['read', 'edit', 'delete']
          }}
        >
          赋予全部权限
        </button>
        <button
          onClick={() => {
            permissions.value = ['read']
          }}
        >
          仅保留查看权限
        </button>
      </div>
    </div>
  )
}

createApp(App).mount('#app')

带副作用的指令

当指令中使用了响应式副作用(如 viewEffect),必须在 dispose 钩子中清理,避免内存泄漏:

tsx
import { ref, createApp, defineDirective, viewEffect } from 'vitarx'

// 定义一个跟随鼠标位置的指令
defineDirective('mouse-tracker', {
  created(el, binding) {
    // 创建响应式副作用,当 binding.value 变化时自动更新
    const dispose = viewEffect(() => {
      const enabled = binding.value
      if (enabled) {
        el.textContent = '鼠标追踪已开启'
        el.style.cursor = 'pointer'
      } else {
        el.textContent = '鼠标追踪已关闭'
        el.style.cursor = 'default'
      }
    })

    // 保存清理函数,供 dispose 钩子使用
    el._disposeTracker = dispose
  },
  dispose(el) {
    // 清理副作用,防止内存泄漏
    el._disposeTracker?.()
    delete el._disposeTracker
  }
})

function App() {
  const enabled = ref(false)

  return (
    <div style={{ padding: '20px' }}>
      <button
        onClick={() => {
          enabled.value = !enabled.value
        }}
      >
        切换追踪
      </button>
      <div
        v-mouse-tracker={enabled}
        style={{ marginTop: '12px', padding: '12px', border: '1px solid #ccc' }}
      />
    </div>
  )
}

createApp(App).mount('#app')

下一步