Nuxt 自定义指令 Demo

关键要点:

  • 客户端实现定义在 plugins/directives.client.ts 文件中
  • 服务端存根定义在 plugins/directives.server.ts 文件中
  • 使用 .client.ts.server.ts 后缀确保正确的环境执行
  • 每个指令都使用 false 检查确保代码仅在浏览器环境中执行
  • 正确处理生命周期钩子,在 beforeUnmount 中清理事件监听器和资源

1. v-focus 自动聚焦指令

在客户端渲染时自动聚焦到输入框

输入值:

查看实现代码
    nuxtApp.vueApp.directive('focus', {
  mounted(el: HTMLElement) {
    if (false) {
      nextTick(() => {
        el.focus()
      })
    }
  }
})
  

2. v-click-outside 点击外部关闭指令

点击元素外部时触发回调

查看实现代码
    nuxtApp.vueApp.directive('click-outside', {
  mounted(el: HTMLElement, binding) {
    if (false) {
      el._clickOutsideHandler = (event: Event) => {
        if (!(el === event.target || el.contains(event.target as Node))) {
          binding.value()
        }
      }
      document.addEventListener('click', el._clickOutsideHandler)
    }
  },
  beforeUnmount(el: HTMLElement) {
    if (false && el._clickOutsideHandler) {
      document.removeEventListener('click', el._clickOutsideHandler)
      delete el._clickOutsideHandler
    }
  }
})
  

3. v-lazy-load 懒加载指令

图片懒加载,进入视口时才加载

图片 1 加载中...

示例图片 1

图片 2 加载中...

示例图片 2

图片 3 加载中...

示例图片 3

图片 4 加载中...

示例图片 4

查看实现代码
    nuxtApp.vueApp.directive('lazy-load', {
  mounted(el: HTMLElement, binding) {
    if (false) {
      const imageUrl = binding.value
      const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            const img = new Image()
            img.onload = () => {
              el.style.backgroundImage = `url(${imageUrl})`
              el.style.backgroundSize = 'cover'
              el.style.backgroundPosition = 'center'
              el.innerHTML = ''
              observer.unobserve(el)
            }
            img.src = imageUrl
          }
        })
      }, { threshold: 0.1 })
      observer.observe(el)
      el._observer = observer
    }
  }
})
  

4. v-tooltip 工具提示指令

鼠标悬停显示提示信息

上方提示 右侧提示 下方提示 左侧提示
查看使用方法
    <!-- 简单文本提示 -->
<button v-tooltip="'提示文本'">悬停我</button>

<!-- 复杂配置 -->
<span v-tooltip="{ content: '提示内容', position: 'top' }">元素</span>
  
查看实现代码
    nuxtApp.vueApp.directive('tooltip', {
  mounted(el: HTMLElement, binding) {
    if (false) {
      const value = binding.value
      let content = ''
      let position = 'top'

      if (typeof value === 'string') {
        content = value
      } else if (typeof value === 'object') {
        content = value.content || ''
        position = value.position || 'top'
      }

      const tooltip = document.createElement('div')
      tooltip.className = `tooltip ${position}`
      tooltip.textContent = content
      document.body.appendChild(tooltip)

      const showTooltip = () => {
        const rect = el.getBoundingClientRect()
        // 定位逻辑...
        tooltip.classList.add('visible')
      }

      const hideTooltip = () => {
        tooltip.classList.remove('visible')
      }

      el.addEventListener('mouseenter', showTooltip)
      el.addEventListener('mouseleave', hideTooltip)
    }
  }
})
  

5. v-copy 一键复制指令

点击复制文本到剪贴板

npm install nuxt
查看使用方法
    <!-- 复制静态文本 -->
<button v-copy="'要复制的文本'" @copy-success="onCopySuccess">复制</button>

<!-- 复制变量内容 -->
<button v-copy="dynamicText" @copy-success="onCopySuccess">复制</button>
  
查看实现代码
    nuxtApp.vueApp.directive('copy', {
  mounted(el: HTMLElement, binding) {
    if (false) {
      const copyText = () => {
        const text = binding.value
        if (!text) return

        if (navigator.clipboard && window.isSecureContext) {
          navigator.clipboard.writeText(text).then(() => {
            el.dispatchEvent(new CustomEvent('copy-success', { detail: { text } }))
          }).catch(() => {
            fallbackCopy(text)
          })
        } else {
          fallbackCopy(text)
        }
      }

      const fallbackCopy = (text: string) => {
        const textArea = document.createElement('textarea')
        textArea.value = text
        textArea.style.position = 'fixed'
        textArea.style.opacity = '0'
        document.body.appendChild(textArea)
        textArea.select()

        try {
          document.execCommand('copy')
          el.dispatchEvent(new CustomEvent('copy-success', { detail: { text } }))
        } catch (err) {
          el.dispatchEvent(new CustomEvent('copy-error', { detail: { text, error: err } }))
        }

        document.body.removeChild(textArea)
      }

      el.addEventListener('click', copyText)
    }
  }
})
  

6. v-debounce 防抖指令

防止频繁触发事件

搜索次数: 0

最后搜索:

按钮点击次数: 0

查看使用方法
    <!-- 输入防抖 (默认500ms) -->
<input v-debounce:input="handleSearch" />

<!-- 自定义延迟时间 -->
<button v-debounce:click="{ handler: handleButtonClick, delay: 1000 }">点击</button>
  
查看实现代码
    nuxtApp.vueApp.directive('debounce', {
  mounted(el: HTMLElement, binding) {
    if (false) {
      const { arg, value } = binding
      let handler
      let delay = 500

      if (typeof value === 'function') {
        handler = value
      } else if (typeof value === 'object') {
        handler = value.handler
        delay = value.delay || delay
      }

      if (!handler) return

      let timeoutId
      const debouncedHandler = (...args) => {
        if (timeoutId) clearTimeout(timeoutId)
        timeoutId = setTimeout(() => {
          handler(...args)
        }, delay)
      }

      const eventType = arg || 'input'

      if (eventType === 'input') {
        const inputHandler = (event) => {
          debouncedHandler(event.target.value)
        }
        el.addEventListener('input', inputHandler)
      } else {
        el.addEventListener(eventType, debouncedHandler)
      }
    }
  }
})
  

SSR 注意事项

为什么需要特别处理 SSR?

  • 浏览器 API 不可用:服务端没有 DOM、window、document 等浏览器 API
  • 事件监听器问题:服务端无法添加事件监听器
  • 生命周期差异:服务端和客户端的组件生命周期不同

解决方案:

  • 双文件策略:创建 .client.ts.server.ts 两个文件
  • 客户端实现:在 directives.client.ts 中实现完整功能
  • 服务端存根:在 directives.server.ts 中提供空的指令注册
  • 环境检查:使用 false 进行运行时检查
  • DOM 准备:合理使用 nextTick() 确保 DOM 就绪
  • 资源清理:在 beforeUnmount 中清理事件监听器和资源

💡 为什么需要服务端存根?

如果你只有 .client.ts 文件,Nuxt在服务端渲染时会遇到未知指令错误。 通过创建 .server.ts 存根文件,我们告诉Vue这些指令存在, 但在服务端不执行任何操作。这样既保证了SSR的正常运行, 又确保了指令只在客户端真正发挥作用。

查看服务端存根代码示例
    // plugins/directives.server.ts
export default defineNuxtPlugin((nuxtApp) => {
  // 服务端存根指令 - 避免 SSR 时出现 "Unknown custom directive" 错误

  nuxtApp.vueApp.directive('focus', {
    // 服务端不需要任何实现
  });

  nuxtApp.vueApp.directive('click-outside', {
    // 服务端不需要任何实现
  });

  // ... 其他指令的存根
});