Vue 自定义指令可以实现哪些有用的功能

Vue 有一些很实用的指令 v-show v-if v-text v-html v-bind v-on 可以帮助我们实现很复杂的功能,同时它还开辟了钩子供我们自己实现自定义指令。根据自己平时开发总结了一些可以通过指令实现的功能场景:

  • 控制页面元素显示与隐藏,可用作控制权限功能
  • 页面元素点击事件防抖与节流
  • 通过自定义指令控制图片懒加载
  • 针对页面特定元素添加自定义行为
  • 对输入内容进行过滤

下面列举一些常见的自定义指令的实现代码:

权限指令

实现页面操作按钮级别的控制,通过查找父元素然后移除它下面子元素来控制按钮的显示与与否

/**
 * 权限指令 
 * @param {string} value 权限标识
 * 例:<div v-permission="'editInfo'"></div>
 */
Vue.directive('permission', {
  inserted: function (el, binding) {
    const { value } = binding
    // 在前置路由拦截获取权限按钮列表后存储在 store 中
    const actionList = store.state.user.permission
    if (value) {
      const hasPermission = actionList .some(btnKey => btnKey === value)
      // 没有权限直接移除 dom元素
      if (!hasPermission) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    } else {
      throw new Error(`需要指定权限标识! 如:v-permission="'editInfo'"`)
    }
  }
})

防抖指令

单位时间只触发最后一次

/**
 * 防抖指令
 * @param {Function} fn - 执行事件
 * @param {?String|"click"} event - 事件类型 例:"click"
 * @param {?Number|500} time - 间隔时间
 * @param {Array} binding.value - [fn,event,time]
 * 直接使用: <XXX v-debounce="reset]">刷新</XXX>
 * 配置事件,间隔时间: <button v-debounce="[reset,'click',500]">刷新</button>
 * 事件传递参数则: <button v-debounce="[()=>reset(param),`click`,500]">刷新</button>
 */
Vue.directive('debounce', {
    bind: function (el, binding) {
        try {
            let fn, event = "click", time = 500;
            if (typeof binding.value == 'function') {
                fn = binding.value
            } else {
                [fn, event = "click", time = 500] = binding.value
            }
            let timer;
            el.addEventListener(event, () => {
                timer && clearTimeout(timer)
                timer = setTimeout(() => fn(), time)
            })
        } catch (e) {
            console.log(e)
        }
    }
})

节流指令

一段时间内首次触发时立即执行,此时间段内再次触发,不会执行

/**
 * 节流指令 
 * @param {Function} fn - 执行事件
 * @param {?String|"click"} event - 事件类型 例:"click"
 * @param {?Number|500} time - 间隔时间
 * @param {Array} binding.value - [fn,event,time]
 * 直接使用: <XXX v-throttle="reset]">刷新</XXX>
 * 配置事件,间隔时间: <button v-throttle="[reset,'click',500]">刷新</button>
 * 事件传递参数则: <button v-throttle="[()=>reset(param),`click`,500]">刷新</button>
 */
Vue.directive('throttle', {
    bind: function (el, binding) {
        let fn, event = "click", time = 1500;
        if (typeof binding.value == 'function') {
            fn = binding.value
        } else {
            [fn, event = "click", time = 1500] = binding.value
        }

        /**
         * el.preTime 记录上次触发事件,
         * 每次触发比较nowTime(当前时间) 和 el.preTime 的差是否大于指定的时间段!
         */
        el.addEventListener(event, () => {
            const nowTime = new Date().getTime()
            if (!el.preTime || nowTime - el.preTime > time) {
                el.preTime = nowTime
                fn();
            }
        })
    }
})

图片懒加载

监听 scroll 事件,然后动态将图片的自定义属性 data-src 的值赋予 src 来达到实际路径的目的

/*
* lazyLoad
* img v-LazyLoad="image1.jpg">
*/
Vue.directive('LazyLoad', {
  bind(el, binding) {
    LazyLoad.init(el, binding.value, defaultSrc)
  },
  inserted(el) {
    if (IntersectionObserver) {
      LazyLoad.observe(el)
    } else {
      LazyLoad.listenerScroll(el)
    }
  },
})
// 初始化
init(el, val, def) {
  el.setAttribute('data-src', val)
  el.setAttribute('src', def)
},
// 利用IntersectionObserver监听el
observe(el) {
  var io = new IntersectionObserver((entries) => {
    const realSrc = el.dataset.src
    if (entries[0].isIntersecting) {
      if (realSrc) {
        el.src = realSrc
        el.removeAttribute('data-src')
      }
    }
  })
  io.observe(el)
},
// 监听scroll事件
listenerScroll(el) {
  const handler = LazyLoad.throttle(LazyLoad.load, 300)
  LazyLoad.load(el)
  window.addEventListener('scroll', () => {
    handler(el)
  })
}

给块元素增加背景

使用 canvas 特性生成 base64 格式的图片文件,设置其字体大小,颜色等,将其设置为背景图片,从而实现页面或组件水印效果

/**
* 添加背景
* <div v-waterMarker="{text: '版权所有,禁止转载', textColor: 'rgba(180, 180, 180, 0.4)'}"></div>
*/
Vue.directive('waterMarker', {
  bind: function (el, binding) {
    addWaterMarker(binding.value.text, el, binding.value.font, binding.value.textColor)
  }
}),
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') + ')'
}

页面元素拖拽指令

设置需要拖拽的元素为相对定位,其父元素为绝对定位
鼠标按下(onmousedown)时记录目标元素当前的 lefttop
鼠标移动(onmousemove)时计算每次移动的横向距离和纵向距离的变化值,并改变元素的 lefttop

/*
* 拖拽指令
* 直接使用 <div class="move-box" v-draggable></div>
*/
Vue.directive('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
      }
    }
  },
})

输入框过滤特殊字符

可通过配置正则来过滤相关的字符输入

/*
* 过滤字符
* <input type="text" v-model="name" v-filterInput />
*/
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)
}
Vue.directive('filterInput', {
  bind: function (el, binding, vnode) {
    // 正则规则可根据需求自定义
    var regRule = /[^u4E00-u9FA5|d|a-zA-Z|rns,.?!,。?!…—&$=()-+/*{}[]]|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)
  }
})

增加长按行为

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

创建一个计时器, 2 秒后执行函数。

当用户按下按钮时触发 mousedown 事件,启动计时器;用户松开按钮时调用 mouseout 事件。
如果 mouseup 事件 2 秒内被触发,就清除计时器,当作一个普通的点击事件。
如果计时器没有在 2 秒内清除,则判定为一次长按,可以执行关联的函数。
在移动端要考虑 touchstarttouchend 事件。

/**
* 长按指令
* <button v-longpress="longpress">长按</button>
**/
Vue.directive('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)
  },
})

评论

这篇文章目前有 一条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注

Sidebar