🍉🍉🍉花里胡哨的vue指令

前言

该文主要是介绍一些常用的vue自定义指令,可以让我们的代码看起来更加简洁。

vue2和vue3的指令写法略有不同,主要区别就是钩子上的调整,如果已经了解过的同学,可以直接跳到demo去看看

简介

除了 Vue 内置的一系列指令 (比如 v-modelv-show) 之外,Vue 还允许你注册自定义的指令 (Custom Directives) 一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。

Vue.js 2和Vue.js 3在自定义指令方面有一些区别。

在Vue.js 2中,自定义指令使用directive选项来定义,并且需要在适当的生命周期钩子函数中编写指令的逻辑。指令的定义包括bindupdateunbind三个钩子函数,分别用于绑定指令时、更新指令时和解绑指令时执行相应的逻辑。

而在Vue.js 3引入了Composition API,使得指令的逻辑可以更容易地组织和重用。你可以使用onBeforeMountonMountedonBeforeUpdateonUpdatedonBeforeUnmount等Composition API钩子函数来编写指令的逻辑。此外,Vue.js 3还引入了新的钩子函数beforeMountmountedbeforeUpdateupdatedunmounted,用于替代Vue.js 2中的bindupdateunbind

vue2、vue3的生命周期对照

钩子函数 Vue.js 2 Vue.js 3
bind 绑定时执行的逻辑 移除,使用beforeMount替代
inserted 元素插入到父节点时执行的逻辑 移除,可以使用mounted来代替
update 元素所在组件的VNode更新时执行的逻辑 使用beforeUpdate和updated替代
componentUpdated 元素所在组件的VNode及其子VNode更新时执行的逻辑 使用beforeUpdate和updated替代
unbind 解绑时执行的逻辑 使用unmounted替代

vue2自定义指令文档
vue3自定义指令文档

vue3 钩子简介

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

常用例子

waterMarker指令(水印)

js 复制代码
function addWaterMarker (str, parentNode, font, textColor) {
  // 水印文字,父元素,字体,文字颜色
  var can = document.createElement('canvas')
  parentNode.appendChild(can)
  can.width = 200
  can.height = 150
  can.style.display = 'none'
  var cans = can.getContext('2d')
  cans.rotate((-20 * Math.PI) / 180)
  cans.font = font || '16px Microsoft JhengHei'
  cans.fillStyle = textColor || 'rgba(180, 180, 180, 0.3)'
  cans.textAlign = 'left'
  cans.textBaseline = 'Middle'
  cans.fillText(str, can.width / 10, can.height / 2)
  parentNode.style.backgroundImage = 'url(' + can.toDataURL('image/png') + ')'
}

const waterMarker = {
  bind: function (el, binding) {
    addWaterMarker(binding.value.text, el, binding.value.font, binding.value.textColor)
  },
}

export default waterMarker
html 复制代码
<script>
import waterMarker from '@/directives/waterMarker'
export default {
  directives: {
    waterMarker
  }
}
</script>

<template>
  <div class="text-box" v-waterMarker="{text:'超神熊猫',textColor:'rgba(0, 0, 0, 0.4)'}"></div>
</template>

copy指令(复制)

js 复制代码
const copy = {
  bind (el, { value }) {
    el.$value = value
    el.handler = () => {
      if (!el.$value) {
        console.log('复制内容为空')
        return
      }
      const textarea = document.createElement('textarea')
      textarea.readOnly = 'readonly'
      textarea.style.position = 'absolute'
      textarea.style.left = '-9999px'
      textarea.value = el.$value
      document.body.appendChild(textarea)
      textarea.select()
      if (navigator.clipboard) {
        navigator.clipboard.writeText(el.$value).then(() => {
          console.log('复制成功', el.$value)
        })
      } else {
        // execCommand即将被废弃
        const result = document.execCommand('Copy')
        if (result) {
          console.log('复制成功', el.$value)
        }
      }
      document.body.removeChild(textarea)
    }
    el.addEventListener('click', el.handler)
  },
  componentUpdated (el, { value }) {
    el.$value = value
  },
  unbind (el) {
    el.removeEventListener('click', el.handler)
  },
}

export default copy

clickOutside指令(点击元素外部)

js 复制代码
const clickOutside = {
  // 初始化指令
  bind(el, binding) {
    function clickHandler(e) {
      if (el.contains(e.target)) {
        return false;
      }
      if (binding.expression) {
        binding.value(e);
      }
    }
    el.__vueClickOutside__ = clickHandler;
    document.addEventListener("click", clickHandler);
  },
  unbind(el) {
    document.removeEventListener("click", el.__vueClickOutside__);
    delete el.__vueClickOutside__;
  },
}

export default clickOutside

longPress指令(长按)

js 复制代码
const longPress = {
  bind: function (el, binding, vNode) {
    if (typeof binding.value !== 'function') {
      throw 'callback must be a function'
    }
    // 定义变量
    let pressTimer = null
    // 创建计时器( 2秒后执行函数 )
    let start = (e) => {
      if (e.type === 'click' && e.button !== 0) {
        return
      }
      if (pressTimer === null) {
        pressTimer = setTimeout(() => {
          handler()
        }, 2000)
      }
    }
    // 取消计时器
    let cancel = (e) => {
      if (pressTimer !== null) {
        clearTimeout(pressTimer)
        pressTimer = null
      }
    }
    // 运行函数
    const handler = (e) => {
      binding.value(e)
    }
    // 添加事件监听器
    el.addEventListener('mousedown', start)
    el.addEventListener('touchstart', start)
    // 取消计时器
    el.addEventListener('click', cancel)
    el.addEventListener('mouseout', cancel)
    el.addEventListener('touchend', cancel)
    el.addEventListener('touchcancel', cancel)
  },
  // 当传进来的值更新的时候触发
  componentUpdated (el, { value }) {
    el.$value = value
  },
  // 指令与元素解绑的时候,移除事件绑定
  unbind (el) {
    el.removeEventListener('click', el.handler)
  },
}
export default longPress

debounce指令(防抖)

js 复制代码
const debounce = {
  inserted: function (el, { value }) {
    // 可支持仅传入函数,也可支持传入函数和延迟时间配置
    let callback = null
    let time = 1000
    if (typeof value == 'function') {
      callback = value
    } else if (value.callback && typeof value.callback == 'function') {
      callback = value.callback
      time = value.time || 1000
    }
    if (!callback) {
      throw 'Must be passed into the callback function'
    }
    let timer = null
    el.addEventListener('click', () => {
      if (timer) {
        clearTimeout(timer)
      }
      timer = setTimeout(() => {
        callback()
      }, time)
    })
  },
}

export default debounce
html 复制代码
<script>
import debounce from '@/directives/debounce'
export default {
  directives: {
    debounce
  },
  methods: {
    debounceClick () {
      console.log('只触发一次')
    }
  }
}
</script>

<template>
  <div>
    <button v-debounce="debounceClick">防抖(默认1秒)</button>
    <button v-debounce="{ callback: debounceClick, time: 2000 }">防抖(传入2秒)</button>
  </div>
</template>

rightClick指令(右键点击)

js 复制代码
const rightClick = {
  bind: function(el, binding, vnode) {
    // 绑定事件处理程序
    el.addEventListener('contextmenu', function(event) {
      event.preventDefault() // 阻止默认的右键菜单显示
      binding.value(event) // 调用指令绑定的回调函数,并传入鼠标事件对象
    })
  }
}
 
export default rightClick
html 复制代码
<script>
import rightClick from '@/directives/rightClick'
export default {
  directives: {
    rightClick
  },
  methods: {
    rightClickFn(event) {
      console.log('点击右键', event)
    }
  }
}
</script>

<template>
  <div>
    <div class="dom-box" v-rightClick="rightClickFn">
      内容
    </div>
  </div>
</template>

右键点击菜单

js 复制代码
const rightClickMenu = {
  bind: function(el, binding, vnode) {
    el.addEventListener('contextmenu', function(event) {
      event.preventDefault()

      if (vnode.context.menuVisible) {
        return false // 如果菜单已经显示,则不再重复弹出
      }
      vnode.context.menuVisible = true // 设置菜单为显示状态

      const { menuItems = [] } = binding.value

      let menu = document.createElement('ul')
      menu.style.position = 'fixed'
      menu.style.zIndex = 999
      menu.style.top = event.clientY + 'px'
      menu.style.left = event.clientX + 'px'

      menuItems.forEach((item) => {
        const menuItem = document.createElement('li')
        menuItem.innerText = item.text
        menuItem.addEventListener('click', function() {
          vnode.context.menuVisible = false // 点击菜单项后隐藏菜单
          item.handler()
        })
        menu.appendChild(menuItem)
      })
      document.body.appendChild(menu)

      function deleteMenu(e) {
        if (!el.contains(e.target)) {
          vnode.context.menuVisible = false 
          if (menu) document.body.removeChild(menu)
          menu = null
        }
      }
      // 点击其他地方时隐藏菜单
      el.__vueDeleteMenu__ = deleteMenu
      document.addEventListener("click", deleteMenu)
    })
  },
  unbind(el) {
    document.removeEventListener("click", el.__vueDeleteMenu__)
    delete el.__vueDeleteMenu__
  },
}
 
export default rightClickMenu
html 复制代码
<script>
import rightClickMenu from '@/directives/rightClickMenu'
export default {
  directives: {
    rightClickMenu
  },
  methods: {
    getRightClickMenu() {
      return {
        menuItems: [
          { id: 1, text: '菜单项1', handler: () => console.log('点击了菜单项1') },
          { id: 2, text: '菜单项2', handler: () => console.log('点击了菜单项2') },
          { id: 3, text: '菜单项3', handler: () => console.log('点击了菜单项3') }
        ]
      }
    }
  }
}
</script>

<template>
  <div>
    <div class="dom-box" v-rightClickMenu="getRightClickMenu()">
      内容
    </div>
  </div>
</template>

文本高亮指令

js 复制代码
const highlight = {
  bind: function (el, binding) {
    setHighlight(el, binding)
  },
  update: function (el, binding) {
    setHighlight(el, binding)
  }
}

function setHighlight(el, binding) {
  const { value } = binding
  let _keywords = value
  let _color = 'red'
  if (typeof value === 'object') {
    const { keywords = '', color = _color } = value
    _keywords = keywords
    _color = color
  }
  if (_keywords) {
    _keywords = _keywords.split('|')
    const regex = new RegExp(`(${_keywords.join('|')})`, 'gi')
    const highlightedText = el.innerText.replace(regex, `<span style="color: ${_color};">$1</span>`)
    el.innerHTML = highlightedText
  } else {
    el.innerHTML = el.innerText
  }
}

export default highlight
html 复制代码
<script>
import highlight from '@/directives/highlight'
export default {
  directives: {
    highlight
  },
  data() {
    return {
      value: ''
    }
  }
}
</script>

<template>
  <div>
    <input type="text" v-model="value">
    <p v-highlight="value">内容:123456789</p>
  </div>
</template>

输入框禁止输入表情符号

js 复制代码
let findEle = (parent, type) => {
  return parent.tagName.toLowerCase() === type ? parent : parent.querySelector(type)
}

const trigger = (el, type) => {
  const e = document.createEvent('HTMLEvents')
  e.initEvent(type, true, true)
  el.dispatchEvent(e)
}

const emoji = {
  bind: function (el, binding, vnode) {
    // 正则规则可根据需求自定义
    var regRule = /[^\u4E00-\u9FA5|\d|\a-zA-Z|\r\n\s,.?!,。?!...---&$=()-+/*{}[\]]|\s/g
    let $inp = findEle(el, 'input')
    el.$inp = $inp
    $inp.handle = function () {
      let val = $inp.value
      $inp.value = val.replace(regRule, '')

      trigger($inp, 'input')
    }
    $inp.addEventListener('keyup', $inp.handle)
  },
  unbind: function (el) {
    el.$inp.removeEventListener('keyup', el.$inp.handle)
  },
}

export default emoji
html 复制代码
<script>
import emoji from '@/directives/emoji'
export default {
  directives: {
    emoji
  },
  data() {
    return {
      value: ''
    }
  },
}
</script>

<template>
  <input type="text" v-emoji v-model="value">
</template>

总结

vue指令可以将很多常用的功能简化封装起来,是否真的有必要封装指令也是值得思考的一件事。有些样式指令(如:增加某个指令可以改变样式之类的),可能还不如直接规范的写好公共样式,指令虽好,但切勿滥用

相关推荐
崔庆才丨静觅27 分钟前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60611 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了1 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅2 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax