Vue 自定义指令: Vue.directive(id, [definition])

参数:

{string}: id
{Function | Object}  [definition]

用法:

// 注册
Vue.directive('my-directive', {
  bind: function () {},
  inserted: function () {},
  update: function () {},
  componentUpdated: function () {},
  unbind: function () {}
})

// 注册 (指令函数)
Vue.directive('my-directive', function () {
  // 这里将会被 `bind` 和 `update` 调用
})

// getter,返回已注册的指令
var myDirective = Vue.directive('my-directive')

自定义指令有五个钩子函数,分别表示的意义如下:

bind: 只调用一次,指令第一次绑定到元素时调用,可以定义一个在绑定时执行一次的初始化动作。
inserted: 被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于 document 中)。
update: 被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值。
componentUpdated: 被绑定元素所在模板完成一次更新周期时调用。
unbind: 只调用一次, 指令与元素解绑时调用。

结合工作中的一些使用场景,我们可以自定义一些常用的指令。

directives.js:

// v-copy
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.display = 'none'
            textarea.style.left = '-9999px'
            textarea.value = el.$value

            document.body.appendChild(textarea)

            textarea.select()

            const result = document.execCommand('Copy')
            if (result) {
                //  可根据UI 另设计
                console.log('复制成功')
            }
            document.body.removeChild(textarea)
        }
        // 绑定点击事件 一键 copy
        el.addEventListener('click', el.handler)
    },
    componentUpdated(el, { value }) {
        el.$value = value
    },
    // 指令与元素解绑时 移除绑定事件
    unbind (el) {
        el.removeEventListener('click', el.handler)
    }
}

/**
 *
 * <template>
        <button v-copy="copyText">复制</button>
    </template>

    <script>
        export default {
            data() {
                return {
                    copyText: 'a copy directives',
                }
            },
        }
    </script>
 */

// v-longpress
const longpress = {
    bind (el, binding, vNode) {
        if (typeof binding.value !== 'function') {
            throw 'callback must be a function'
        }
        let pressTimer = null

        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)
    }
}
/**
 * <template>
  <button v-longpress="longpress">长按</button>
</template>

<script>
export default {
  methods: {
    longpress () {
      alert('长按指令生效')
    }
  }
}
</script>
 * 
 */

// 防抖函数限制规定 规定时间内之只能点击一次
const debounce = {
    inserted (el, binding) {
        let timer
        el.addEventListener('click', () => {
            if (timer) {
                clearTimeout(timer)
            } 
            timer = setTimeout(() => {
                binding.value()
            }, 1000)
        })
    }
}
/**
 * <template>
  <button v-debounce="debounceClick">防抖</button>
</template>

<script>
export default {
  methods: {
    debounceClick () {
      console.log('只触发一次')
    }
  }
}
</script>
 */

// 限制输入内容 不能输入表情和特殊字符,只能时数字或字母等 常规方法就是在 change 中做正则校验
/**
 * <template>
  <input type="text" v-model="note" @change="vaidateEmoji" />
</template>

<script>
  export default {
    methods: {
      vaidateEmoji() {
        var reg = /[^u4E00-u9FA5|d|a-zA-Z|
s,.?!,。?!…—&$=()-+/*{}[]]|s/g
        this.note = this.note.replace(reg, '')
      },
    },
  }
</script>
 */
// 按照正则校验的规则来设计一个指令
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 (el, binding, vNode) {
        // 正则规则可根据需求自定义
        var regRule = /[^u4E00-u9FA5|d|a-zA-Z|
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 (el) {
        el.$inp.removeEventListener('keyup', el.$inp.handle)
    }
}
/**
 * <template>
  <input type="text" v-model="note" v-emoji />
</template>
 */

// 实现一个图片懒加载指令,只加载浏览器可见区域的图片
/**
 *  图片懒加载的原理主要是判断当前图片是否到了可视区域这一核心逻辑实现的
      拿到所有的图片 Dom ,遍历每个图片判断当前图片是否到了可视区范围内
        如果到了就设置图片的 src 属性,否则显示默认图片    
        图片懒加载有两种方式可以实现,一是绑定 srcoll 事件进行监听,
        二是使用 IntersectionObserver 判断图片是否到了可视区域,但是有浏览器兼容性问题。

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

const Lazyload = {
    install (Vue, options) {
        const defalutSrc = options.default
        Vue.directive('lazy', {
            bind(el, binding) {
                Lazyload.init(el, binding.value, defalutSrc)
            },
            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)
        })
    },
    // 加载真实图片
    load (el) {
        const windowHeight = document.documentElement.clientHeight
        const elTop = el.getBoundingClientRect().top
        const elBtm = el.getBoundingClientRect().bottom
        const realSrc = el.dataset.src
        if (elTop - windowHeight < 0 && elBtm > 0) {
            if (realSrc) {
                el.src = realSrc
                el.removeAttribute('data-src')
            }
        }
    },
    // 节流
    throttle (fn, delay) {
        let timer
        let prevTime
        return function (...args) {
            const currTime = Date.now()
            const context = this
            if (!prevTime) prevTime = currTime
            clearTimeout(timer)

            if (currTime - prevTime > delay) {
                prevTime = currTime
                fn.apply(context, args)
                clearTimeout(timer)
                return
            }

            timer = setTimeout(() => {
                prevTime = Date.now()
                timer = null
                fn.apply(context, args)
            }, delay)
        }
    }
}
/**
 * <img v-LazyLoad="xxx.jpg" />
 */

/**
 * 需求:自定义一个权限指令,对需要权限判断的 Dom 进行显示隐藏。
     自定义一个权限数组
   判断用户的权限是否在这个数组内,如果是则显示,否则则移除 Dom
 */

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 (el, binding) {
        let permission = binding.value
        if (permission) {
            let hasPermission = checkArray(permission)
            if (!hasPermission) {
                el.parentNode && el.parentNode.removeChild(el)
            }
        }
    }
}
/**
 * <div class="btns">
  <!-- 显示 -->
  <button v-permission="'1'">权限按钮1</button>
  <!-- 不显示 -->
  <button v-permission="'10'">权限按钮2</button>
</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 (el, binding) {
        addWaterMarker(binding.value.text, el, binding.value.font, binding.value.textColor)
    }
}
/**
 *  <template>
            <div v-waterMarker="{text:'lzg版权所有',textColor:'rgba(180, 180, 180, 0.4)'}"></div>
        </template>
 */

/**
 * 实现一个拖拽指令,可以在页面可视区域任意拖拽元素
 * 
 * 设置需要拖拽的元素为相对定位,其父元素为绝对定位。
    鼠标按下(onmousedown)时记录目标元素当前的 left 和 top 值。
    鼠标移动(onmousemove)时计算每次移动的横向距离和纵向距离的变化值,并改变元素的 left 和 top 值
    鼠标松开(onmouseup)时完成一次拖拽
 */
const draggable = {
    inserted (el) {
        el.style.cursor = 'move'
        el.onmosedown = 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
            }
        }
    }
}
/**
 * <template>
        <div class="el-dialog" v-draggable></div>
    </template>
 */


export default {
    copy,
    longpress,
    debounce,
    emoji,
    Lazyload,
    permission,
    waterMarker,
    draggable
}

引入方法:

新建 directives/index.js 文件:

import {
    copy,
    longpress,
    debounce,
    emoji,
    Lazyload,
    permission,
    waterMarker,
    draggable
} from './directives'

const directives = {
    copy,
    longpress,
    debounce,
    emoji,
    Lazyload,
    permission,
    waterMarker,
    draggable
}

export default {
    install(Vue) {
        Object.keys(directives).forEach((key) => {
            Vue.directive(key, directives[key])
        })
    }
}

在 main.js 中注册 Directives 插件:

import Vue from 'vue'
import Directives from './directives'
Vue.use(Directives)

 注:内容来自于网络,侵删。