参考: 6个实用的Vue自定义指令推荐
1、长按指令 v-longpress
需求: 当用户按下鼠标左键或移动端单指触碰,并按住按钮几秒钟时,视为一次长按,触发对应的函数。
思路:
- 定义一个计时器, n 秒后执行函数,n作为参数。
- 当用户按下按钮时触发 mousedown 或 touchstart 事件,启动计时器。
- 如果 click 、 mouseup 、touchend 或 touchcancel 事件在 n 秒内被触发,则清除计时器,视为普通点击事件。
- 如果计时器没有在 n秒内清除,则视为一次长按,触发对应的函数。
longpress.js
/**
* 长按指令
* 需求:当用户按下鼠标左键或移动端单指触碰,并按住按钮几秒钟时,视为一次长按,触发对应的函数。
* 思路:
* 1. 定义一个计时器,n 秒后执行函数,n 作为参数。
* 2. 当用户按下按钮时触发 mousedown 或 touchstart 事件,启动计时器。
* 3. 如果 click、mouseup、touchend 或 touchcancel 事件在 n 秒内被触发,则清除计时器,视为普通点击事件。
* 4. 如果计时器没有在 n 秒内清除,则视为一次长按,触发对应的函数。
*/
const longpress = {
bind: function (el, {value:{fn,time}}) {
// 没绑定函数直接返回
if (typeof fn !== 'function') return
// 定义定时器变量
el._timer = null
// 创建计时器( n秒后执行函数 )
el._start = (e) => {
// e.type表示触发的事件类型如 mousedown,touchstart 等
// pc端: e.button表示是哪个键按下。0为鼠标左键,1为中键,2为右键
// 移动端: e.touches表示同时按下的键为个数
if (
(e.type === 'mousedown' && e.button && e.button !== 0) ||
(e.type === 'touchstart' && e.touches && e.touches.length > 1)
) return;
// 定时长按n秒后执行事件
if (el._timer === null) {
el._timer = setTimeout(() => {
fn()
}, time)
// 取消浏览器默认事件,如右键弹窗
el.addEventListener('contextmenu', function(e) {
e.preventDefault();
})
}
}
// 如果两秒内松手,则取消计时器
el._cancel = (e) => {
if (el._timer !== null) {
clearTimeout(el._timer)
el._timer = null
}
}
// 添加计时监听
el.addEventListener('mousedown', el._start)
el.addEventListener('touchstart', el._start)
// 添加取消监听
el.addEventListener('click', el._cancel)
el.addEventListener('mouseout', el._cancel)
el.addEventListener('touchend', el._cancel)
el.addEventListener('touchcancel', el._cancel)
},
// 指令与元素解绑时,移除事件绑定
unbind(el) {
// 移除计时监听
el.removeEventListener('mousedown', el._start)
el.removeEventListener('touchstart', el._start)
// 移除取消监听
el.removeEventListener('click', el._cancel)
el.removeEventListener('mouseout', el._cancel)
el.removeEventListener('touchend', el._cancel)
el.removeEventListener('touchcancel', el._cancel)
},
}
export default longpress
directives/index.js
// 导入指令文件
import longpress from "./longpress"; // 长按指令
import debounce from "./debounce"; // 函数防抖指令
import throttle from "./throttle"; // 函数节流指令
import clickOut from "./clickOut"; // 点击元素外部指令
import scrollPop from "./scrollPop"; // 弹窗限制外部滚动指令
import focus from "./focus"; // 聚焦指令
import copy from "./copy"; // 复制粘贴指令
import permission from "./permission"; // 权限校验指令
import waterMarker from "./waterMarker"; // 实现页面水印效果
import draggable from "./draggable"; // 拖拽指令
import emoji from "./emoji"; // 禁止表情及特殊字符
// 集成一起
const directives = {
longpress,
debounce,
throttle,
clickOut,
scrollPop,
focus,
copy,
permission,
waterMarker,
draggable,
emoji
}
// 批量注册
const install = function(Vue) {
Object.keys(directives).forEach((key) => {
Vue.directive(key, directives[key])
})
}
export default install
main.js: 在 main.js 引入,并 Vue.use()
调用完成批量注册。
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// 将自动注册所有组件为全局组件
import dataV from '@jiaminghi/data-view'
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';
// 1.引入:引入移动事件(el-drag-dialog.js)
import elDragDialog from './directives/dialog/el-drag-dialog';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import directives from './directives/index';
// 2.注册:添加标签事件绑定 可以放大移动弹窗
Vue.directive('el-drag-dialog', elDragDialog);
// 弹窗默认点击遮罩层会关闭弹框,现改为不关闭,为了防止和拖拽冲突,这句需要放在use ElementUI之前(也可以不加这句,自己测试区别)
ElementUI.Dialog.props.closeOnClickModal.default = false;
Vue.use(Antd);
Vue.use(ElementUI);
Vue.use(dataV);
Vue.use(directives);
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
使用: 给 dom 加上 v-longpress
及参数即可。
<template>
<button v-longpress="{fn: longpress, time: 2000}">长按</button>
</template>
<script>
export default {
methods: {
longpress () {
console.log('长按指令生效')
}
}
}
</script>
2、函数防抖指令(输入框防抖) v-debounce
背景: 在开发中,有时遇到要给 input
或者滚动条添加监听事件,需要做防抖处理。
需求: 防止 input
或 scroll
事件在短时间内被多次触发,使用防抖函数限制一定时间后触发。
思路:
- 定义一个延迟执行的方法,如果在延迟时间内再调用该方法,则重新计算执行时间。
- 将事件绑定在传入的方法上。
debounce.js
/**
* v-debounce 函数防抖指令 单位时间内只触发最后一次
* @param {Function} fn - 执行事件
* @param {?String|"click"} event - 事件类型 例如: "click"、"input"
* @param {?Number|500} time - 间隔时间
* @param {Object} binding.value - {fn, event, time}
* 例:<input v-model="inputVal" v-debounce="{fn: debounce, event: 'input', time: 2000}" placeholder="函数防抖指令使用" />
* <el-button v-debounce="{fn: reset}">刷新</el-button>
*/
const debounce = {
inserted: function (el, binding) {
let {fn, event = "click", time = 500 } = binding.value;
// 没绑定函数直接返回
if (typeof fn !== 'function') return
el._timer = null
// 监听点击事件,限定时间内如果再次点击则清空定时器并重新定时
el.addEventListener(event, () => {
if (el._timer !== null) {
clearTimeout(el._timer)
el._timer = null
}
el._timer = setTimeout(() => {
fn()
}, time)
})
},
}
export default debounce
使用: 给 dom 加上 v-debounce
及参数即可。
<template>
<!-- @input="debounce" -->
<input v-model="inputVal" v-debounce="{fn: debounce, event: 'input', time: 2000}" />
<div v-debounce="{fn: debounce, event: 'scroll', time: 2000}">
<p>函数防抖指令函数防抖指令函数防抖指令函数防抖指令函数防抖指令...</p>
</div>
</template>
<script>
export default {
data() {
return {
inputVal: ''
}
},
methods: {
debounce(){
console.log('debounce 防抖');
console.log(this.inputVal);
},
}
}
</script>
3、函数节流指令 v-throttle
背景: 在开发中,有些提交保存按钮有时候会在短时间内被点击多次,这样就会多次重复请求后端接口,造成数据的混乱,比如立即购买按钮,多次点击就会多次调用创建订单接口。
需求: 防止按钮在短时间内被多次点击,使用节流函数限制规定时间内只能点击一次。
思路:
- 定义一个由开关(默认为开)控制是否执行的方法,第一次执行函数时将开关关闭,在规定时间内再调用该方法,则不会再次执行,直至规定时间过后开关打开。
- 将事件绑定在 click 方法上。
throttle.js
/**
* v-throttle 函数节流指令
* @param {Function} fn - 执行事件
* @param {?Number|500} time - 间隔时间
* @param {Object} binding.value - {fn, time}
* 例:
* <button v-throttle="{fn: throttle, time: 2000}">throttle节流</button>
*/
const throttle = {
bind:function (el, binding) {
let { fn, time = 500 } = binding.value;
if (typeof fn !== 'function') return
el._flag = true;// 开关默认为开
el._timer = null
el.handler = function () {
if (!el._flag) return;
// 执行之后开关关闭
el._flag && fn()
el._flag = false
if (el._timer !== null) {
clearTimeout(el._timer)
el._timer = null
}
el._timer = setTimeout(() => {
el._flag = true; // 几秒后开关开启
}, time);
}
el.addEventListener('click',el.handler)
},
unbind:function (el,binding) {
el.removeEventListener('click',el.handler)
}
}
export default throttle
使用: 给 dom 加上 v-throttle
及参数即可。
<template>
<!-- @click="throttle" -->
<button v-throttle="{fn: throttle, time: 3000}">throttle节流</button>
</template>
<script>
export default {
methods: {
throttle () {
console.log('throttle 节流 只触发一次')
}
}
}
</script>
4、点击元素外部指令 v-click-out
背景: 在我们的项目里,经常会出现一个弹窗,需要点击弹窗外部关闭该弹窗。
需求: 实现一个指令,点击目标区域外部,触发指定函数。
思路:
- 判断点击的元素是否为目标元素,是则不作为,否则触发指定函数。
clickOut.js
/**
* 点击元素外部指令
*/
const clickOut = {
bind(el, {value}){
function clickHandler(e) {
// contains 判断某个元素是不是选定元素的子元素或者本身
// 先判断点击的元素是否是本身,如果是本身,则返回
if (el.contains(e.target)) return;
// 判断指令中是否绑定了函数
if (typeof value === 'function') {
// 如果绑定了函数,则调用函数,此处 value 就是 clickImgOut方法
value()
}
}
// 给当前元素绑定个私有变量,方便在unbind中可以解除事件监听
el.handler = clickHandler;
//添加事件监听
setTimeout(() => {
document.addEventListener('click',el.handler);
}, 0);
},
unbind(el){
//解除事件监听
document.removeEventListener('click',el.handler);
}
}
export default clickOut
使用: 将需要用到该指令的元素添加 v-click-out
。
注意: 使用 v-if ,不使用 v-show,否则无效。
<template>
<div class="about">
<div>
<button @click="dialogVisibleTwo = true">展示弹窗</button>
<div v-if="dialogVisibleTwo" v-click-out="clickDialogOut">
<div>标题</div>
<div>内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
dialogVisibleTwo: false,
}
},
mounted() { },
methods: {
clickDialogOut() {
this.dialogVisibleTwo = false;
console.log('点击弹窗外部');
},
}
};
</script>
5、自动聚焦指令 v-focus
需求: 在开发中,有时候需要 input 输入框自动聚焦。
思路: 使用 focus() 方法。
focus.js
/**
* 自动聚焦指令
*/
const focus = {
// 当被绑定的元素插入到DOM中时
inserted: function(el) {
// 聚焦元素
el.focus();
}
}
export default focus
使用: 将需要用到该指令的元素添加 v-focus
。
<input v-model="inputVal" v-focus placeholder="自动聚焦" />
6、复制粘贴指令 v-copy
需求: 实现点击复制按钮,一键复制文本内容,用于鼠标右键粘贴。
当用户通过浏览器 UI(例如,使用 Ctrl/⌘+C 键盘快捷方式或从菜单中选择“复制”)启动复制操作并响应允许的 document.execCommand('copy')
调用时触发copy事件。
思路:
- 动态创建 textarea 标签,并设置 readOnly 属性及移出可视区域。
- 将要复制的值赋给 textarea 标签的 value 属性,并插入到 body。
- 选中值 textarea 并复制。通过原生 JS 方法
document.execCommand('copy')
。 - 将 body 中插入的 textarea 移除。
- 在第一次调用时,绑定事件,在解绑时,移除事件。
copy.js
/**
* 复制粘贴指令
*/
const copy = {
bind(el, { value }) {
el.$value = value
el.handler = () => {
if (!el.$value) {
// 值为空的时候,给出提示。可根据项目UI仔细设计
console.log('无复制内容')
return
}
// 动态创建 textarea 标签
const textarea = document.createElement('textarea')
// 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
textarea.readOnly = 'readonly'
textarea.style.position = 'absolute'
textarea.style.left = '-9999px'
// 将要 copy 的值赋给 textarea 标签的 value 属性
textarea.value = el.$value
// 将 textarea 插入到 body 中
document.body.appendChild(textarea)
// 选中值并复制
// select() 方法用于选择该元素中的文本。
textarea.select()
const result = document.execCommand('Copy')
if (result) {
console.log('复制成功') // 可根据项目UI仔细设计
}
document.body.removeChild(textarea)
}
// 绑定点击事件,就是所谓的一键 copy 啦
el.addEventListener('click', el.handler)
},
// 当传进来的值更新的时候触发
componentUpdated(el, { value }) {
el.$value = value
},
// 指令与元素解绑的时候,移除事件绑定
unbind(el) {
el.removeEventListener('click', el.handler)
},
}
export default copy
使用: 给 dom 加上 v-copy
及复制的文本即可。
<template>
<button v-copy="copyText">复制</button>
</template>
<script>
export default {
data() {
return {
copyText: 'a copy directives',
}
},
}
</script>
7、权限校验指令 v-permission
背景: 在一些后台管理系统,我们可能需要根据用户角色进行一些操作权限的判断,很多时候我们都是粗暴地给一个元素添加 v-if / v-show
来进行显示隐藏,但如果判断条件繁琐且多个地方需要判断,这种方式的代码不仅不优雅而且冗余。针对这种情况,我们可以通过全局自定义指令来处理。
需求: 自定义一个权限指令,对需要权限判断的 Dom 进行显示隐藏。
思路:
- 自定义一个权限数组。
- 判断用户的权限是否在这个数组内,如果是,则显示,否则,移除 Dom。
permission.js
/**
* v-permission 权限校验指令
*/
function checkArray(key) {
let arr = ['1', '2', '3', '4']
let index = arr.indexOf(key)
if (index > -1) {
return true // 有权限
} else {
return false // 无权限
}
}
const permission = {
inserted: function (el, binding) {
let permission = binding.value // 获取到 v-permission 的值
if (permission) {
let hasPermission = checkArray(permission)
if (!hasPermission) {
// 没有权限 移除Dom元素
el.parentNode && el.parentNode.removeChild(el)
}
} else {
console.log(`请设置操作权限标签值`);
}
},
}
export default permission
使用: 给 v-permission
赋值判断即可。
<div>
<!-- 显示 -->
<button v-permission="'1'">权限按钮1</button>
<!-- 不显示 -->
<button v-permission="'10'">权限按钮2</button>
</div>
8、实现页面水印指令 v-waterMarker
需求: 给整个页面添加背景水印。
思路:
- 使用 canvas 特性生成 base64 格式的图片文件,设置其字体大小,颜色等。
- 将其设置为背景图片,从而实现页面或组件水印效果。
waterMarker.js
/**
* v-waterMarker 实现页面水印指令
* @param {*} str 水印文字
* @param {*} parentNode 父元素
* @param {*} font 字体
* @param {*} textColor 文字颜色
* 例子:
* <div style="width: 100%; height: 300px" v-waterMarker="{text: 'xxx版权所有', textColor: 'rgba(180, 180, 180, 0.5)'}"></div>
*/
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
使用: 给需要添加水印的 dom 加上 v-waterMarker
,并设置水印文案、颜色、字体大小等。
<div
style="width: 100%; height: 300px"
v-waterMarker="{text: 'xxx版权所有', textColor: 'rgba(180, 180, 180, 0.5)'}"
>
内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容
</div>
9、拖拽指令 v-draggable
需求: 实现一个拖拽指令,可在页面可视区域任意拖拽元素。
思路:
- 设置需要拖拽的元素为绝对定位,其父元素为相对定位。
- 鼠标按下(
onmousedown
)时,记录目标元素当前的 left 和 top 值。 - 鼠标移动(
onmousemove
)时,计算每次移动的横向距离和纵向距离的变化值,并改变元素的 left 和 top 值。 - 鼠标松开(
onmouseup)
时,完成一次拖拽。
draggable.js
/**
* v-draggable 拖拽指令
*/
const draggable = {
inserted: function (el) {
el.style.cursor = 'move'
el.onmousedown = function (e) {
let disx = e.pageX - el.offsetLeft
let disy = e.pageY - el.offsetTop
document.onmousemove = function (e) {
let x = e.pageX - disx
let y = e.pageY - disy
let maxX = document.body.clientWidth - parseInt(window.getComputedStyle(el).width)
let maxY = document.body.clientHeight - parseInt(window.getComputedStyle(el).height)
if (x < 0) {
x = 0
} else if (x > maxX) {
x = maxX
}
if (y < 0) {
y = 0
} else if (y > maxY) {
y = maxY
}
el.style.left = x + 'px'
el.style.top = y + 'px'
}
document.onmouseup = function () {
document.onmousemove = document.onmouseup = null
}
}
},
}
export default draggable
使用: 在 dom 上加上 v-draggable
即可实现拖动。
<div
style="position: absolute; top: 10px; left: 10px; width: 100px; height: 100px; background-color: pink;"
v-draggable
>可拖拽Div</div>
拖拽指令:v-drag
export default {
inserted(el) {
// let maxLeft = el.parentNode.clientWidth - el.clientWidth;
// let maxTop = el.parentNode.clientHeight - el.clientHeight;
let maxLeft = document.body.clientWidth - el.clientWidth;
let maxTop = document.body.clientHeight - el.clientHeight;
el.onmousedown = function (e) {
// el.style.cursor = 'move'
var disx = e.pageX - el.offsetLeft;
var disy = e.pageY - el.offsetTop;
document.onmousemove = function (e) {
let left = e.pageX - disx;
let top = e.pageY - disy;
el.style.left = left + "px";
el.style.top = top + "px";
if (left <= 0) {
el.style.left = 0 + "px";
}
if (left >= maxLeft) {
el.style.left = maxLeft + "px";
}
if (top <= 0) {
el.style.top = 0 + "px";
}
if (top >= maxTop) {
el.style.top = maxTop + "px";
}
};
document.onmouseup = function () {
document.onmousemove = document.onmouseup = null;
// el.style.cursor = 'default';
};
};
}
}