Vue3:根据el-input封装全局v-focus指令

发布于:2025-09-13 ⋅ 阅读:(15) ⋅ 点赞:(0)

问:

/**
 * v-focus
 * 按钮权限指令
 */
import type { Directive } from 'vue'
import { nextTick } from 'vue'

const vFocus: Directive = {
  async mounted(el: HTMLElement) {
    setTimeout(() => {
      const input = el.querySelector<HTMLInputElement>('.el-input__inner') || el.querySelector('input') || el
      // 微任务延迟确保渲染完成
      if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {
        input.focus() // 直接聚焦找到的元素
        // 更新ElementPlus输入框样式
        const wrapper = input.closest('.el-input')
        wrapper?.classList.add('is-focus')
      }
    }, 200)
  },
}

export default vFocus
请问这段代码是我封装的自定义v-focus方法永爱自动触发el-input的输入框的光标的,但是现在进入页面只能触发一次,关闭弹框后再次打开弹框el-input的光标就不会自动触发了,请问怎么回事怎么解决

回答:

问题复现

  1. 页面第一次打开弹框v-focus 能正常把 el-input(或普通 <input>)聚焦并给外层 .el-input 加上 is-focus 样式。
  2. 关闭弹框(弹框的 DOM 被 v-if 或 v-show 隐藏/销毁)后再次打开,光标不再自动出现

这通常是因为 指令只在元素第一次挂载 (mounted) 时执行,而弹框关闭后 Vue 并没有重新触发 mounted(如果是 v-show 只切换 display,DOM 并未销毁),或者弹框是通过 v-if 销毁后又重新创建,但 指令的 mounted 回调中的 setTimeout 已经在第一次挂载后就被清理掉,导致后续实例没有再执行聚焦逻辑。


1. 了解指令的生命周期钩子

钩子 何时调用
created 指令第一次绑定到元素上(还未挂载到 DOM)
beforeMount 元素即将挂载到 DOM(mounted 之前)
mounted 元素挂载到 DOM,el 已经可用
beforeUpdate VNode 更新前(属性/子节点可能变化)
updated VNode 更新后,DOM 已经重新渲染
beforeUnmount 指令即将被卸载(元素即将从 DOM 移除)
unmounted 指令已经被卸载(元素已从 DOM 中移除)

关键点:如果弹框是通过 v-show 隐藏/显示,元素始终保持挂载状态,只会触发 updated(因为 show/hide 会导致属性变化)。如果是 v-if,每次打开都会重新创建元素,mounted 会再次执行。


2. 为什么只能聚焦一次?

场景 A:使用 v-show 隐藏弹框

  • v-show 只改动 display不会触发 mounted
  • 你的指令只在 mounted 时聚焦一次,弹框再次显示时指令不再运行,导致光标不出现。

场景 B:使用 v-if,但指令内部的 setTimeout 被缓存

  • v-if 销毁后再次创建,mounted 会再次执行。
  • 但是 setTimeout 的延迟(200 ms)可能在弹框内部的动画或 nextTick 完成前就执行,导致聚焦的目标元素仍未渲染完毕,从而失效。
  • 另外,如果弹框内部还有 el-input 的 v-model 异步更新,nextTick 需要再等一次微任务。

3. 推荐的解决方案

下面提供 三种可行的实现,任选其一即可。
(1)把聚焦逻辑放到 updated/beforeUpdate,配合 nextTick 确保 DOM 已经渲染完;
(2)在指令里监听弹框的 打开事件(如 @open@after-enter);
(3)使用 v-focus + v-if 的组合,确保每次弹框创建时都走 mounted

3.1 最简版:在 mounted + updated 中都执行一次

import type { Directive } from 'vue'
import { nextTick } from 'vue'

function focusInput(el: HTMLElement) {
  // 1. 先找最可能的输入框
  const input =
    el.querySelector<HTMLInputElement>('.el-input__inner') ||
    el.querySelector('input') ||
    el

  // 2. 必须是可聚焦的表单控件
  if (!(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)) return

  // 3. 使用 nextTick 确保所有子组件已渲染
  nextTick(() => {
    input.focus()
    const wrapper = input.closest('.el-input')
    wrapper?.classList.add('is-focus')
  })
}

const vFocus: Directive = {
  // 第一次挂载(适用于 v-if 重建的情况)
  mounted(el) {
    // 加一点延迟防止弹框动画未完成
    setTimeout(() => focusInput(el), 200)
  },

  // 当父组件重新渲染(v-show 切换 display)时再次尝试聚焦
  // updated 会在每次 VNode 更新后触发
  updated(el) {
    // 这里不需要再加 setTimeout,直接 nextTick 即可
    focusInput(el)
  },

  // 可选:在指令被销毁时清理可能残留的 class
  unmounted(el) {
    const input = el.querySelector('input')
    const wrapper = input?.closest('.el-input')
    wrapper?.classList.remove('is-focus')
  },
}

export default vFocus
为什么可行?
  • mounted 负责 首次v-if)的聚焦,保留原来的 200 ms 延迟以兼容弹框动画。
  • updated 会在 每次弹框显示/隐藏(v-show)或内部状态变化时触发,内部使用 nextTick 等待子组件渲染完毕,再聚焦。
  • 这样无论是 v-show 还是 v-if,指令都会在合适的时机再次执行聚焦逻辑。

3.2 进阶版:监听弹框的 after-enter(如果使用 Element Plus ElDialog

如果你使用的是 Element Plus 的 ElDialog(或者其他基于 transition 的弹框),可以在指令内部监听弹框的 transitionend/after-enter 事件,确保动画结束后再聚焦:

const vFocus: Directive = {
  mounted(el) {
    const dialog = el.closest('.el-dialog')
    if (dialog) {
      // 当弹框打开动画结束后触发
      const onAfterEnter = () => {
        focusInput(el)
        dialog?.removeEventListener('after-enter', onAfterEnter)
      }
      dialog.addEventListener('after-enter', onAfterEnter)
    } else {
      // 非弹框场景,直接聚焦
      setTimeout(() => focusInput(el), 200)
    }
  },

  // 同样保留 updated 以防 v-show 场景
  updated(el) {
    focusInput(el)
  },
}

注意ElDialog 在内部会把 after-enter 事件挂在根元素(.el-dialog__wrapper),因此需要 closest('.el-dialog') 或者直接在弹框组件上绑定 @after-enter="..." 并在外层调用 focus()


3.3 另一种思路:使用 nextTick + requestAnimationFrame

有时候弹框的渲染顺序非常快,setTimeout(200) 可能仍然早于真实可聚焦的时机。可以把 两层异步组合起来:

function focusWhenReady(el: HTMLElement) {
  nextTick(() => {
    requestAnimationFrame(() => {
      const input = /* 同上 */ 
      if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {
        input.focus()
        input.closest('.el-input')?.classList.add('is-focus')
      }
    })
  })
}

nextTick → 微任务,随后 requestAnimationFrame → 下一帧渲染,基本可以保证输入框已经真正出现在页面上。


4. 小技巧:防止重复添加 is-focus

如果弹框打开多次,is-focus 类可能会一直残留在上一次的 DOM 节点上,导致样式不统一。可以在聚焦前先 移除 再 添加

const wrapper = input.closest('.el-input')
wrapper?.classList.remove('is-focus')
wrapper?.classList.add('is-focus')

或者在 unmounted/beforeUnmount 时统一清理。


5. 完整示例(整合所有建议)

import type { Directive } from 'vue'
import { nextTick } from 'vue'

function focusInput(el: HTMLElement) {
  const input =
    el.querySelector<HTMLInputElement>('.el-input__inner') ||
    el.querySelector('input') ||
    el

  if (!(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)) return

  nextTick(() => {
    // 让浏览器在下一帧真正渲染后再聚焦
    requestAnimationFrame(() => {
      input.focus()
      const wrapper = input.closest('.el-input')
      wrapper?.classList.remove('is-focus')
      wrapper?.classList.add('is-focus')
    })
  })
}

const vFocus: Directive = {
  mounted(el) {
    // 如果是弹框组件,等弹框动画结束再聚焦
    const dialog = el.closest('.el-dialog')
    if (dialog) {
      const handler = () => {
        focusInput(el)
        dialog.removeEventListener('after-enter', handler)
      }
      dialog.addEventListener('after-enter', handler)
    } else {
      // 普通场景保持原来的延迟
      setTimeout(() => focusInput(el), 200)
    }
  },

  // 兼容 v-show 切换或内部数据变化
  updated(el) {
    focusInput(el)
  },

  // 清理样式,防止残留
  unmounted(el) {
    const input = el.querySelector('input')
    const wrapper = input?.closest('.el-input')
    wrapper?.classList.remove('is-focus')
  },
}

export default vFocus

使用方式

<template>
  <el-dialog v-model="show" title="编辑">
    <el-input v-focus v-model="name" placeholder="请输入姓名" />
  </el-dialog>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import vFocus from '@/directives/v-focus'

const show = ref(false)
const name = ref('')
</script>

6. 小结

关键点 说明
指令只在 mounted 执行一次 对 v-show 隐藏的弹框不起作用
使用 updated(或 beforeUpdate 每次弹框显示/内容变化时重新聚焦
配合 nextTick + requestAnimationFrame 确保 DOM 已完成渲染后才调用 .focus()
如果有弹框动画 监听弹框的 after-enter/transitionend 再聚焦
清理 is-focus 防止类名残留导致样式异常

把上述任意一种实现方式替换掉你原来的 vFocus就可以在弹框每次打开时都自动聚焦 el-input,不会再出现“只能触发一次”的情况。祝开发愉快 🎉!