Vue 自定义指令

一、使用场景

只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。
一个常见例子是使元素获取焦点的 v-focus 指令。该指令比 autofocus 属性更有用,因为它不仅在页面加载时有效,而且在 Vue 动态插入元素时也有效!

<script setup>
// 在模板中启用 v-focus
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

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

二、注册

局部注册

组合式
<script setup>
// 在模板中启用 v-highlight
const vHighlight = {
  mounted: (el) => {
    el.classList.add('is-highlight')
  }
}
</script>

<template>
  <p v-highlight>This sentence is important!</p>
</template>
选项式
export default {
  setup() {
    /*...*/
  },
  directives: {
    // 在模板中启用 v-focus
    focus: {
      /* ... */
    }
  }
}

全局注册

const app = createApp({})

// 使 v-focus 在所有组件中都可用
app.directive('focus', {
  /* ... */
})

三、钩子函数

const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode) {}
}

四、钩子函数参数

指令的钩子会传递以下几种参数:

  • el:指令绑定到的元素。这可以用于直接操作 DOM。
  • binding:一个对象,包含以下属性。
    • value:传递给指令的值。例如在 v-my-directive=“1 + 1” 中,值是 2。
    • oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 “foo”。
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }。
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode。
  • prevVnode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。

❗❗❗除了 el 外,其他参数都是只读的,不要更改它们。若你需要在不同的钩子间共享信息,推荐通过元素的 dataset attribute 实现。

五、应用示例

1、按钮权限验证

功能描述: 项目根据登录用户所配置的角色权限,对按钮进行权限控制,拥有权限的按钮才能够触发点击事件,没有权限的按钮触发时进行提示。

/**
 * # 验证用户权限, 并进行提示
 *
 * @example
 * ```vue
 * <el-button
 *   v-auth="{
 *     code: AuthCode.UPLOAD_FILE,
 *     event: handleUploadConfirm,
 *   }"
 * >
 *   确认上传
 * </el-button>
 * ```
 *
 * @param code 权限 code
 */
export default <Directive>{
  mounted(el: HTMLElement, bindings) {
    if (!bindings.value) {
      throw new Error('value 不能为空')
    }
    if (typeof bindings.value !== 'object') {
      throw new Error('value 必须为对象')
    }
    const { code, event } = bindings.value
    el.addEventListener(bindEvent, () => {
      if (!checkAuth(code)) {
        return
      }
      if (typeof event === 'function') {
        event()
      }
    })
  }
}

/**
 * @param code 权限 code
 * @returns
 */
function checkAuth(code: string) {
	...
}

2、自定义用户行为收集指令

功能描述:需要对项目中用户的行为进行收集,以备后续的日志记录查询或者用户行为分析。 比如记录哪个用户,在什么时间,在哪个页面,触发了什么功能。支持按钮按钮行为收集、非按钮点击行为收集。在项目入口写定时器,每隔一定时间调接口将用户行为存到数据库。

/**
 * @example
 * <el-link
 *   type="primary"
 *   v-collect
 *   @click="getRoleAuthList(row)"
 * >
 *   查看
 * </el-link>
 * 
 * <el-button
 *   size="large"
 *   type="primary"
 *   v-collect
 *   @click="handleClickMove"
 * >
 *   新增项目
 * </el-button>
 * 
 * 自定义用户行为收集指令
 */
export default <Directive>{
  mounted(el: HTMLElement, bindings) {
    el.addEventListener('mousedown', e => {
      e.stopPropagation()
      const collect = getCollect()
      collect.push({
        ...
      })
      setCollect(collect)
    })
  }
}


// /utils/collect.ts
const collectKey = 'USER_SELECT'

function getCollect(): Collect[] {
  const collect = window.localStorage.getItem(collectKey) || '[]'
  return JSON.parse(collect)
}

function setCollect(collect: Collect[]) {
  window.localStorage.setItem(collectKey, JSON.stringify(collect))
}

3、按钮点击防抖

功能描述:点击按钮进行操作时,防止用户短时间点击多次触发多次事件,为按钮点击事件添加防抖功能,默认 1 秒以内重复点击无效。实现方案是点击按钮触发一次事件,给按钮设置 disabaled 属性为 true,1 秒后删除该限制。

/**
 * @example
 * <el-button v-clicked:500></el-button>
 * 
 * @param delay number类型, 设置禁止点击的时间间隔, 默认1000
 */

export default {
  mounted(el: HTMLElement | HTMLButtonElement, bindings: DirectiveBinding) {
    let timer: number | null = null
    let delay = 1000
    const oldEvents = el.style.pointerEvents
    if (bindings.arg) {
      if (!/^\d+$/.test(bindings.arg)) {
        throw new Error('参数必须是数字')
      }
      delay = Number(bindings.arg)
    }
    el.addEventListener('click', setDisabled)
    function setDisabled() {
      el.setAttribute('disabled', 'true')
      // el.classList.add('is-disabled')
      if (el.tagName !== 'BUTTON') {
        el.style.pointerEvents = 'none' // 点击不到按钮,穿透到下层元素
      }
      if (timer) {
        window.clearTimeout(timer)
        timer = null
      }
      timer = window.setTimeout(() => {
        // el.classList.remove('is-disabled')
        el.removeAttribute('disabled')
        el.style.pointerEvents = oldEvents
      }, delay)
    }
  }
}

4、输入框自动获取焦点

功能描述:页面加载之后自动聚焦到某个输入框,使输入框处于输入状态

/**
* @example
* <el-input v-focus></el-input>
*/
export default {
  mounted(el: HTMLElement | HTMLInputElement) {
    let ipt: HTMLInputElement | null
    if (el.nodeName === 'INPUT') {
      ipt = el as HTMLInputElement
    } else {
      ipt = el.querySelector('.el-input__inner') as HTMLInputElement
    }
    ipt && (ipt as HTMLInputElement).focus()
  }
}

5、输入框自动去空字符串

interface HTMLElementPlus extends HTMLElement {
  _handler: (e: KeyboardEvent) => void
  _ele: HTMLElementPlus
}
export default {
  mounted(el: HTMLElementPlus) {
    let ipt
    if (el.nodeName === 'INPUT') {
      ipt = el
    } else {
      ipt = el.querySelector('.el-input__inner')
    }
    const handler = (e: KeyboardEvent) => {
      if (e.code === 'Space') {
        e.preventDefault()
      }
    }
    el._ele = ipt as HTMLElementPlus
    el._handler = handler
    ;(ipt as HTMLElementPlus).addEventListener('keydown', handler)
  },
  unmounted(el: HTMLElementPlus) {
    const { _ele } = el
    _ele.removeEventListener('keydown', _ele._handler)
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值