Vue自定义指令

除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令。

自动注册指令

新建 directives/index.js文件

1
import copy from './copy'
2
import longpress from './longpress'
3
import permission from './permission'
4
import debounce from './debounce'
5
import emoji from './emoji'
6
import LazyLoad from './LazyLoad'
7
import waterMarker from './waterMarker'
8
import draggable from './draggable'
9
// 自定义指令
10
const directives = {
11
    copy,
12
    longpress,
13
    permission,
14
    debounce,
15
    emoji,
16
    LazyLoad,
17
    waterMarker,
18
    draggable
19
}
20
export default {
21
    install(Vue) {
22
        Object.keys(directives).forEach((key) => {
23
            Vue.directive(key, directives[key])
24
        })
25
    },
26
}

main.js 文件引入

1
import Vue from 'vue'
2
import directives from '@/directives'
3
Vue.use(directives)

指令列表

  • 复制粘贴指令 v-copy
  • 长按指令 v-longpress
  • 输入框防抖指令 v-debounce
  • 禁止表情及特殊字符 v-emoji
  • 图片懒加载 v-LazyLoad
  • 权限校验指令 v-premission
  • 实现页面水印 v-waterMarker
  • 拖拽指令 v-draggable

v-copy

实现一键复制文本内容,用于鼠标右键粘贴。

  1. 动态创建 textarea 标签,并设置 readOnly 属性及移出可视区域
  2. 将要复制的值赋给 textarea 标签的 value 属性,并插入到 body
  3. 选中值 textarea 并复制
  4. 将 body 中插入的 textarea 移除
  5. 在第一次调用时绑定事件,在解绑时移除事件
1
const copy = {
2
  bind(el, { value }) {
3
    el.$value = value
4
    el.handler = () => {
5
      if (!el.$value) {
6
        // 值为空的时候,给出提示。可根据项目UI仔细设计
7
        console.log('无复制内容')
8
        return
9
      }
10
      // 动态创建 textarea 标签
11
      const textarea = document.createElement('textarea')
12
      // 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
13
      textarea.readOnly = 'readonly'
14
      textarea.style.position = 'absolute'
15
      textarea.style.left = '-9999px'
16
      // 将要 copy 的值赋给 textarea 标签的 value 属性
17
      textarea.value = el.$value
18
      // 将 textarea 插入到 body 中
19
      document.body.appendChild(textarea)
20
      // 选中值并复制
21
      textarea.select()
22
      const result = document.execCommand('Copy')
23
      if (result) {
24
        console.log('复制成功') // 可根据项目UI仔细设计
25
      }
26
      document.body.removeChild(textarea)
27
    }
28
    // 绑定点击事件,就是所谓的一键 copy 啦
29
    el.addEventListener('click', el.handler)
30
  },
31
  // 当传进来的值更新的时候触发
32
  componentUpdated(el, { value }) {
33
    el.$value = value
34
  },
35
  // 指令与元素解绑的时候,移除事件绑定
36
  unbind(el) {
37
    el.removeEventListener('click', el.handler)
38
  },
39
}
40
41
export default copy

使用:

1
<template>
2
  <button v-copy="copyText">复制</button>
3
</template>
4
<script> 
5
export default {
6
    data() {
7
      return {
8
        copyText: 'a copy directives',
9
      }
10
    },
11
} 
12
</script>

v-longpress

实现长按,用户需要按下并按住按钮几秒钟,触发相应的事件

  1. 创建一个计时器, 2 秒后执行函数
  2. 当用户按下按钮时触发 mousedown 事件,启动计时器;用户松开按钮时调用 mouseout 事件。
  3. 如果 mouseup 事件 2 秒内被触发,就清除计时器,当作一个普通的点击事件
  4. 如果计时器没有在 2 秒内清除,则判定为一次长按,可以执行关联的函数。
  5. 在移动端要考虑 touchstart,touchend 事件
1
const longpress = {
2
  bind: function (el, binding, vNode) {
3
    if (typeof binding.value !== 'function') {
4
      throw 'callback must be a function'
5
    }
6
    // 定义变量
7
    let pressTimer = null
8
    // 创建计时器( 2秒后执行函数 )
9
    let start = (e) => {
10
      if (e.type === 'click' && e.button !== 0) {
11
        return
12
      }
13
      if (pressTimer === null) {
14
        pressTimer = setTimeout(() => {
15
          handler()
16
        }, 2000)
17
      }
18
    }
19
    // 取消计时器
20
    let cancel = (e) => {
21
      if (pressTimer !== null) {
22
        clearTimeout(pressTimer)
23
        pressTimer = null
24
      }
25
    }
26
    // 运行函数
27
    const handler = (e) => {
28
      binding.value(e)
29
    }
30
    // 添加事件监听器
31
    el.addEventListener('mousedown', start)
32
    el.addEventListener('touchstart', start)
33
    // 取消计时器
34
    el.addEventListener('click', cancel)
35
    el.addEventListener('mouseout', cancel)
36
    el.addEventListener('touchend', cancel)
37
    el.addEventListener('touchcancel', cancel)
38
  },
39
  // 当传进来的值更新的时候触发
40
  componentUpdated(el, { value }) {
41
    el.$value = value
42
  },
43
  // 指令与元素解绑的时候,移除事件绑定
44
  unbind(el) {
45
    el.removeEventListener('click', el.handler)
46
  },
47
}
48
49
export default longpress

使用:

1
<template>
2
  <button v-longpress="longpress">长按</button>
3
</template>
4
5
<script> 
6
export default {
7
  methods: {
8
    longpress () {
9
      alert('长按指令生效')
10
    }
11
  }
12
}
13
</script>

v-debounce

防止按钮在短时间内被多次点击,使用防抖函数限制规定时间内只能点击一次

  1. 定义一个延迟执行的方法,如果在延迟时间内再调用该方法,则重新计算执行时间。
  2. 将时间绑定在 click 方法上。
1
const debounce = {
2
  inserted: function (el, binding) {
3
    let timer
4
    el.addEventListener('keyup', () => {
5
      if (timer) {
6
        clearTimeout(timer)
7
      }
8
      timer = setTimeout(() => {
9
        binding.value()
10
      }, 1000)
11
    })
12
  },
13
}
14
15
export default debounce

使用:

1
<template>
2
  <button v-debounce="debounceClick">防抖</button>
3
</template>
4
5
<script> 
6
export default {
7
  methods: {
8
    debounceClick () {
9
      console.log('只触发一次')
10
    }
11
  }
12
} 
13
</script>

v-emoji

开发中遇到的表单输入,往往会有对输入内容的限制,比如不能输入表情和特殊字符,只能输入数字或字母等。
根据正则表达式,设计自定义处理表单输入规则的指令,下面以禁止输入表情和特殊字符为例。

1
let findEle = (parent, type) => {
2
  return parent.tagName.toLowerCase() === type ? parent : parent.querySelector(type)
3
}
4
5
const trigger = (el, type) => {
6
  const e = document.createEvent('HTMLEvents')
7
  e.initEvent(type, true, true)
8
  el.dispatchEvent(e)
9
}
10
11
const emoji = {
12
  bind: function (el, binding, vnode) {
13
    // 正则规则可根据需求自定义
14
    var regRule = /[^u4E00-u9FA5|d|a-zA-Z|rns,.?!,。?!…—&$=()-+/*{}[]]|s/g
15
    let $inp = findEle(el, 'input')
16
    el.$inp = $inp
17
    $inp.handle = function () {
18
      let val = $inp.value
19
      $inp.value = val.replace(regRule, '')
20
21
      trigger($inp, 'input')
22
    }
23
    $inp.addEventListener('keyup', $inp.handle)
24
  },
25
  unbind: function (el) {
26
    el.$inp.removeEventListener('keyup', el.$inp.handle)
27
  },
28
}
29
30
export default emoji

使用:

1
<template>
2
  <input type="text" v-model="note" v-emoji />
3
</template>

v-LazyLoad

实现一个图片懒加载指令,只加载浏览器可见区域的图片。

  1. 图片懒加载的原理主要是判断当前图片是否到了可视区域这一核心逻辑实现的
  2. 拿到所有的图片 Dom ,遍历每个图片判断当前图片是否到了可视区范围内
  3. 如果到了就设置图片的 src 属性,否则显示默认图片

图片懒加载有两种方式可以实现,一是绑定 srcoll 事件进行监听,
二是使用 IntersectionObserver 判断图片是否到了可视区域,但是有浏览器兼容性问题。

下面封装一个懒加载指令兼容两种方法,判断浏览器是否支持 IntersectionObserver API,
如果支持就使用 IntersectionObserver 实现懒加载,否则则使用 srcoll 事件监听 + 节流的方法实现。

1
const LazyLoad = {
2
  // install方法
3
  install(Vue, options) {
4
    const defaultSrc = options.default
5
    Vue.directive('lazy', {
6
      bind(el, binding) {
7
        LazyLoad.init(el, binding.value, defaultSrc)
8
      },
9
      inserted(el) {
10
        if (IntersectionObserver) {
11
          LazyLoad.observe(el)
12
        } else {
13
          LazyLoad.listenerScroll(el)
14
        }
15
      },
16
    })
17
  },
18
  // 初始化
19
  init(el, val, def) {
20
    el.setAttribute('data-src', val)
21
    el.setAttribute('src', def)
22
  },
23
  // 利用IntersectionObserver监听el
24
  observe(el) {
25
    var io = new IntersectionObserver((entries) => {
26
      const realSrc = el.dataset.src
27
      if (entries[0].isIntersecting) {
28
        if (realSrc) {
29
          el.src = realSrc
30
          el.removeAttribute('data-src')
31
        }
32
      }
33
    })
34
    io.observe(el)
35
  },
36
  // 监听scroll事件
37
  listenerScroll(el) {
38
    const handler = LazyLoad.throttle(LazyLoad.load, 300)
39
    LazyLoad.load(el)
40
    window.addEventListener('scroll', () => {
41
      handler(el)
42
    })
43
  },
44
  // 加载真实图片
45
  load(el) {
46
    const windowHeight = document.documentElement.clientHeight
47
    const elTop = el.getBoundingClientRect().top
48
    const elBtm = el.getBoundingClientRect().bottom
49
    const realSrc = el.dataset.src
50
    if (elTop - windowHeight < 0 && elBtm > 0) {
51
      if (realSrc) {
52
        el.src = realSrc
53
        el.removeAttribute('data-src')
54
      }
55
    }
56
  },
57
  // 节流
58
  throttle(fn, delay) {
59
    let timer
60
    let prevTime
61
    return function (...args) {
62
      const currTime = Date.now()
63
      const context = this
64
      if (!prevTime) prevTime = currTime
65
      clearTimeout(timer)
66
67
      if (currTime - prevTime > delay) {
68
        prevTime = currTime
69
        fn.apply(context, args)
70
        clearTimeout(timer)
71
        return
72
      }
73
74
      timer = setTimeout(function () {
75
        prevTime = Date.now()
76
        timer = null
77
        fn.apply(context, args)
78
      }, delay)
79
    }
80
  },
81
}
82
83
export default LazyLoad

使用: 将组件内 标签的 src 换成 v-LazyLoad

1
<img v-LazyLoad="xxx.jpg" />

v-permission

自定义一个权限指令,对需要权限判断的 Dom 进行显示隐藏。

  1. 获取一个权限数组数据
  2. 判断用户的权限是否在这个数组内,如果是则显示,否则则移除 Dom
1
import Store from '@/store'
2
3
const permission = {
4
    inserted: function (el, binding) {
5
        const value = binding.value;
6
        if (value) {
7
            const permission = Store.getters.permission;
8
            if (!permission.includes(value)) {
9
                el.parentNode && el.parentNode.removeChild(el)
10
            }
11
        }
12
    },
13
}
14
15
export default permission

使用:

1
<div class="btns">
2
  <!-- 显示 -->
3
  <button v-permission="'admin:user:edit'">编辑用户</button>
4
  <!-- 不显示 -->
5
  <button v-permission="'admin:user:destroy'">删除用户</button>
6
</div>

vue-waterMarker

给整个页面添加背景水印

  1. 使用 canvas 特性生成 base64 格式的图片文件,设置其字体大小,颜色等。
  2. 将其设置为背景图片,从而实现页面或组件水印效果
1
function addWaterMarker(str, parentNode, font, textColor) {
2
  // 水印文字,父元素,字体,文字颜色
3
  var can = document.createElement('canvas')
4
  parentNode.appendChild(can)
5
  can.width = 200
6
  can.height = 150
7
  can.style.display = 'none'
8
  var cans = can.getContext('2d')
9
  cans.rotate((-20 * Math.PI) / 180)
10
  cans.font = font || '16px Microsoft JhengHei'
11
  cans.fillStyle = textColor || 'rgba(180, 180, 180, 0.3)'
12
  cans.textAlign = 'left'
13
  cans.textBaseline = 'Middle'
14
  cans.fillText(str, can.width / 10, can.height / 2)
15
  parentNode.style.backgroundImage = 'url(' + can.toDataURL('image/png') + ')'
16
}
17
18
const waterMarker = {
19
  bind: function (el, binding) {
20
    addWaterMarker(binding.value.text, el, binding.value.font, binding.value.textColor)
21
  },
22
}
23
24
export default waterMarker

使用:设置水印文案,颜色,字体大小即可

1
<template>
2
  <div v-waterMarker="{text:'版权所有',textColor:'rgba(180, 180, 180, 0.4)'}"></div>
3
</template>

v-draggable

实现一个拖拽指令,可在页面可视区域任意拖拽元素。

  1. 设置需要拖拽的元素为相对定位,其父元素为绝对定位。
  2. 鼠标按下(onmousedown)时记录目标元素当前的 left 和 top 值。
  3. 鼠标移动(onmousemove)时计算每次移动的横向距离和纵向距离的变化值,并改变元素的 left 和 top 值
  4. 鼠标松开(onmouseup)时完成一次拖拽
1
const draggable = {
2
  inserted: function (el) {
3
    el.style.cursor = 'move'
4
    el.onmousedown = function (e) {
5
      let disx = e.pageX - el.offsetLeft
6
      let disy = e.pageY - el.offsetTop
7
      document.onmousemove = function (e) {
8
        let x = e.pageX - disx
9
        let y = e.pageY - disy
10
        let maxX = document.body.clientWidth - parseInt(window.getComputedStyle(el).width)
11
        let maxY = document.body.clientHeight - parseInt(window.getComputedStyle(el).height)
12
        if (x < 0) {
13
          x = 0
14
        } else if (x > maxX) {
15
          x = maxX
16
        }
17
18
        if (y < 0) {
19
          y = 0
20
        } else if (y > maxY) {
21
          y = maxY
22
        }
23
24
        el.style.left = x + 'px'
25
        el.style.top = y + 'px'
26
      }
27
      document.onmouseup = function () {
28
        document.onmousemove = document.onmouseup = null
29
      }
30
    }
31
  },
32
}
33
export default draggable

使用:

<template>
  <div class="el-dialog" v-draggable></div>
</template>

参考文章:
作者:lzg9527 链接:https://segmentfault.com/a/1190000038475001


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!