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

相关推荐
Мартин.3 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
一 乐4 小时前
学籍管理平台|在线学籍管理平台系统|基于Springboot+VUE的在线学籍管理平台系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·学习
昨天;明天。今天。4 小时前
案例-表白墙简单实现
前端·javascript·css
数云界4 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd4 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常4 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer4 小时前
Vite:为什么选 Vite
前端
小御姐@stella4 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing4 小时前
【React】增量传输与渲染
前端·javascript·面试
eHackyd4 小时前
前端知识汇总(持续更新)
前端