前言
Vue除了核心功能默认内置的指令(v-model
和v-show
)之外,Vue也允许注册自定义指令。
⚠️在Vue2.0中,代码复用和抽象的主要形式是组件,然而,有的情况下,我们仍然需要对普通的DOM元素进行底层操作,这个时候就会用到自定义指令
一个指令定义对象可以提供如下几个钩子函数(均为可选):
bind
:只调用一次,指令第一次绑定到元素时调用,在这里可以进行一次性的初始化设置;
inserted
:被绑定元素插入到父节点时调用(仅保证父节点存在,但不一定已插入到文档中)
update
:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。指令值可能发生了改变,也可能没有,但是可以通过比较更新前后的值来忽略不必要的模版更新。
componentUpdated
:指令所在组件的VNode及其子VNode全部更新后调用。
unbind
:只调用一次,指令与元素解绑时调用。
具体的使用,见链接https://cn.vuejs.org/v2/guide/custom-directive.html,这里就不在继续重复描述
批量注册指令
一般地,我们在src目录中新建目录+文件directives/index.js
,然后在index.js中对外暴露api
1 2 3 4 5 6 7 8 9 10 11 12
| import Vue from 'vue'; import copy from './modules/copy'; import longpress from './modules/longpress'; const directives = { copy, longpress }; export default{ install(Vue){ Object.keys(directives).forEach(key => Vue.directive(key, directives[key])) } }
|
然后在main.js
中引入并调用
1 2 3
| import Vue from 'vue'; import Directives from '@/directives'; Vue.use(Directives);
|
share几个实用的Vue自定义指令
复制粘贴指令:v-copy
需求:实现一键复制文本内容,用于鼠标右键粘贴。
思路:
1. 动态创建textarea
标签,并设置readOnly
属性以及移出可视区域
2. 将要复制的值赋给textarea
标签的value
属性,并插入到body中
3. 选中值textarea
并复制
4. 将body
中插入textarea
移除
5. 在第一次调用时绑定事件,在解绑时移除事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| export default { 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(); const result = document.execCommand('Copy'); if(result){ console.log('复制成功'); } document.body.removeChild(textarea); }; el.addEventListener('click', el.handler); }, componentUpdated(el, { value }){ el.$value = value; },
unbind(el){ el.removeEventListener('click', el.handler); } }
|
用法见👇
1 2 3 4 5 6 7 8 9 10 11 12
| <template> <button v-copy="copyText">复制</button> </template> <script> export default { data(){ return { copyText: '等待被复制的内容' } } } </script>
|
#### 长按指令:v-longpress
需求:实现长按,用户需要按下并屏住按钮几秒钟,触发对应的事件
思路:
1. 创建一个计时器,3秒后执行函数
2. 当用户按下按钮时,触发mousedown
事件,启动计时器;用户松开按钮时调用mouseout
事件
3. 如果mouseup
事件3秒内被触发,就清楚计时器,当作一个普通的点击事件
4. 如果计时器没有在3秒内清楚,则判断为一次长按,并执行捆绑的函数
5. 在移动端则需要考虑touchstart
,touchend
事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| export default { bind(el, {value}){ if('function' !== typeof value){ throw 'callback must be a function'; } let pressTimer = null; let start = e => { if('click' === e.type && 0 !== e.button){ return; } if(null === pressTimer){ pressTimer = setTimeout(() => { value(e); }, 3000); } }; let cancel= e => { if(null !== pressTimer){ clearTimeout(pressTimer); pressTimer = null; } }; el.addEventListener('mousedown', start); el.addEventListener('mouseout', cancel); el.addEventListener('click', cancel); el.addEventListener('touchstart', start); el.addEventListener('touchend', cancel); el.addEventListener('touchcancel', cancel); } }
|
用法见👇
1 2 3 4 5 6 7 8 9 10 11 12
| <template> <button v-longpress="longpress">长按我</button> </template> <script> export default { methods: { longpress(){ alert('触发了长按动作'); } } } </script>
|
#### 输入框防抖指令:v-debounce
背景:在开发中,有些提交保存按钮有时候会在短时间内被点击多次,这样就会多次重复请求后端接口,造成数据的混乱,比如新增表单的提交按钮,多次点击就会新增多条重复的数据。
需求:防止按钮在短时间内被多次点击,使用防抖函数限制规定时间内只能点击一次。
思路:
1. 定义一个延迟执行的方法,如果在延迟时间内再调用该方法,则重新开始延迟时间
2. 在延迟的时间到了之后,执行到click方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export default { inserted(el, { value}){ if('function' !== typeof value){ throw 'directive value must be function'; } let timer; el.addEventListener('keyup', () => { timer && clearTimeout(timer); timer = setTimeout(() => { value&&value(); }, 1000); }); } }
|
用法见👇
1 2 3 4 5 6 7 8 9 10 11 12
| <template> <button v-debounce="debounceAction">防抖</button> </template> <script> export default{ methods: { debounceAction(){ console.info('触发了一次'); } } } </script>
|
#### 禁止表情以及特殊字符:v-emoji
背景:开发中遇到的表单输入,往往会对输入内容的限制,比如不能输入表情和特殊字符,只能输入数字或字母等,我们常规方式是在每一个表单的on-change
事件上做处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <template> <input type="text" v-model="txt" @change="validateEmoji"> </template> <script> export default { data(){ return { txt: '' } }, methods: { validateEmoji(){ let reg = /[^\u4E00-\u9FA5|\d|\a-zA-Z|\r\n\s,.?!,。?!…—&$=()-+/*{}[\]]|\s/g; this.txt = this.txt.replace(reg, ''); } } } </script>
|
这样的代码量比较大且不好维护,因此我们需要自定义一个指令来解决这个问题
需求:根据正则表达式,设计自定义处理表单输入规则的指令,下面以禁止输入表情和特殊字符为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const findEle = (parent, type) => 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); }; export default { bind(el){ let reg = /[^\u4E00-\u9FA5|\d|\a-zA-Z|\r\n\s,.?!,。?!…—&$=()-+/*{}[\]]|\s/g; let $inp = findEle(el, 'input'); el.$inp = $inp; $inp.handle = () => { let val = $inp.value; $inp.value=val.replace(reg, ''); trigger($inp, 'input'); }; }, unbind(el){ el.$inp.removeEventListener('keyup', el.$inp.handle); } }
|
使用:将需要校验的输入框加上v-emoji
即可
1 2 3
| <template> <input type="text" v-model="txt" v-emoji> </template>
|
#### 图片懒加载: v-lazyload
背景:在一些后台管理系统中,我们可能需要根据用户角色进行一些操作权限的判断,很多时候,我们都是简单粗暴地给一个元素添加v-if/v-show
来进行显示隐藏,但如果判断条件繁琐且多个地方需要判断,这种方式的代码不仅
不优雅而且冗余,针对这种情况,我们可以通过全局定义指令来处理。
需求:自定义一个权限指令,对需要权限判断的Dom进行显示/隐藏
思路:
1. 自定义个一个权限组
2. 判断用户的权限是否在这个数组内,如果是则显示,否则移除Dom
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function checkPermission(key){ return ['1', '2', '3', '4'].indexOf(key); } export default { inserted(el, { value }){ if(value){ let hasPermission = checkPermission(value); if(!hasPermission){ el.parentNode && el.parentNode.removeChild(el); } } } };
|
👇是对应的使用方式
1 2 3 4 5 6
| <template> <!-- 显示 --> <button v-permission="1">权限1</button> <!-- 隐藏 --> <button v-permission="10">隐藏</button> </template>
|
#### 实现页面水印:v-waterMarker
需求:给整个页面添加背景水印
思路:
1. 使用canvas
特性生成base64
格式的图片文件,设置其字体大小,颜色等。
2. 将生成的图片文件设置为背景图片,从而实现页面或组件水印效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| export default {
bind(el, { value }){ let canvas = document.createElement('canvas'); el.appendChild(canvas); canvas.width=200; canvas.height=150; canvas.style.display = 'none'; let pen = canvas.getContext('2d'); pen.rotate((-20 * Math.PI) / 180); pen.font = value.font || '16px Microsoft JhengHei'; pen.fillStyle = value.textColor || 'rgba(180, 180, 180, 255)'; pen.textAlign='left'; pen.textBaseline='Middle'; pen.fillText(value.text, canvas.width/10, canvas.height/2); el.style.backgroundImage = `url(${canvas.toDataURL('image/png')})`; } }
|
👇是对应的使用方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <template> <div v-waterMaker="waterMaker"></div> </template> <script> export default{ data(){ return { waterMaker: { text: 'zgl版权所有', textColor: 'rgba(180, 180, 180, 0.4)' } } } } </script>
|
#### 拖拽指令:v-draggable
需求:实现一个拖拽指令,可在页面可视区域任意拖拽元素。
思路:
- 设置需要拖拽的元素为绝对定位,其父元素为相对定位
- 鼠标按下
(onmousedown)
时记录目标元素当前的left
和top
值
- 鼠标移动
(onmousemove)
时计算每次移动的横向以及纵向距离的变化值,并改变元素的left
和top
值
- 鼠标松开
(onmouseup)
时完成一个拖拽1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| export default{ inserted(el){ el.style.cursor = 'move'; el.onmousedown = e => { let disx = e.pageX - el.offsetLeft; let disy = e.pageY - el.offsetTop; document.onmousemove = 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); x < 0 ? x = 0: x > maxX ? x = maxX : ''; y < 0 ? y = 0: y > maxY ? y = maxY : ''; el.style.left = `${x}px`; el.style.top = `${y}px`; }; document.onmouseup = () => { document.onmousemove = document.onmouseup = null; }; }; } }
|
👇是对应的使用方式1 2 3
| <template> <div class="xxx" v-draggable></div> </template>
|