前言
该文主要是介绍一些常用的vue自定义指令,可以让我们的代码看起来更加简洁。
vue2和vue3的指令写法略有不同,主要区别就是钩子上的调整,如果已经了解过的同学,可以直接跳到demo去看看
简介
除了 Vue 内置的一系列指令 (比如 v-model
或 v-show
) 之外,Vue 还允许你注册自定义的指令 (Custom Directives) 一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。
Vue.js 2和Vue.js 3在自定义指令方面有一些区别。
在Vue.js 2中,自定义指令使用directive
选项来定义,并且需要在适当的生命周期钩子函数中编写指令的逻辑。指令的定义包括bind
、update
、unbind
三个钩子函数,分别用于绑定指令时、更新指令时和解绑指令时执行相应的逻辑。
而在Vue.js 3引入了Composition API,使得指令的逻辑可以更容易地组织和重用。你可以使用onBeforeMount
、onMounted
、onBeforeUpdate
、onUpdated
、onBeforeUnmount
等Composition API钩子函数来编写指令的逻辑。此外,Vue.js 3还引入了新的钩子函数beforeMount
、mounted
、beforeUpdate
、updated
、unmounted
,用于替代Vue.js 2中的bind
、update
、unbind
。
vue2、vue3的生命周期对照
钩子函数 | Vue.js 2 | Vue.js 3 |
---|---|---|
bind | 绑定时执行的逻辑 | 移除,使用beforeMount替代 |
inserted | 元素插入到父节点时执行的逻辑 | 移除,可以使用mounted来代替 |
update | 元素所在组件的VNode更新时执行的逻辑 | 使用beforeUpdate和updated替代 |
componentUpdated | 元素所在组件的VNode及其子VNode更新时执行的逻辑 | 使用beforeUpdate和updated替代 |
unbind | 解绑时执行的逻辑 | 使用unmounted替代 |
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指令可以将很多常用的功能简化封装起来,是否真的有必要封装指令也是值得思考的一件事。有些样式指令(如:增加某个指令可以改变样式之类的),可能还不如直接规范的写好公共样式,指令虽好,但切勿滥用