准备工作
创建目录文件:src/directive/index.js
import copy from "./copy"; import longpress from "./longpress" import debounce from "./debounce"; import emoji from "./emoji"; import LazyLoad from "./LazyLoad"; import permission from "./permission"; import waterMarker from "./waterMarker"; import draggable from "./draggable"; import empty from "./empty"; const directives = { copy,longpress,debounce,emoji,LazyLoad,permission,waterMarker,draggable,empty }; export default { install(Vue) { Object.keys(directives).forEach((key) => { Vue.directive(key, directives[key]); }); }, };
然后再main.js中引入并注册
// 引入全局自定义指令 import directive from './directive' Vue.use(directive)
注释:自定义指令的钩子函数
- bind: 只调用一次,指令第一次绑定到元素时使用,可以用这个钩子函数定义一个在绑定时执行一次的初始动作
- inserted: 被绑定的元素插入父节点时调用,(父节点存在即可调用,不必存在于document中)
- update: 被绑定元素所在的模板更新时调用,而不论绑定值是否变化,通过比较更新前后的变化,可以忽略掉不必要的更新
- componentUpdated: 被绑定元素所在模板完成一次更新周期时调用
- unbind: 只调用一次,指令与元素解绑时调用
钩子函数的参数的注意事项:
- el: 指令所绑定的元素,可以用来直接操作DOM
- binding: 一个对象,包含以下属性:
- name: 指令名,不包括v-前缀
- value: 指令的绑定值,这个值就是使用的时候传进来的,如果是指就直接复制,如果是函数可以直接调用
- oldValue: 指令绑定的上一个值,仅在update和componentUpdated中可用,无论值是否改变都可用
- expression: 绑定值的表达式或变量名,
- arg: 传给指令的参数,例如v-my-directive:foo,arg的值是”foo”
- modifiers:一个包含修饰符的对象,例如v-my-directive.foo.bar,修饰符对象modifiers的值是{foo:true,bar:true}
- vnode:Vue编译生成的虚拟节点。
- oldVnode:上一个虚拟节点,仅在update和componentUpdated钩子中可用。
除了 el 之外,其它参数都应该是只读的,切勿进行修改。
一键复制
需求: 实现一键复制文本内容,用于鼠标右键粘贴
思路:
动态创建 textarea 标签,并设置 readOnly 属性及移出可视区域
将要复制的值赋给 textarea 标签的 value 属性,并插入到 body
选中值 textarea 并复制
将 body 中插入的 textarea 移除
在第一次调用时绑定事件,在解绑时移除事件
export default { 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); // 选中值并复制 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); }, };
data(){ return { copyContent: "复制出来的信息", } }
一键长按
需求:
实现长按,用户需要按下并按住按钮几秒钟,触发相应的事件
思路:
创建一个计时器, 2 秒后执行函数
当用户按下按钮时触发 mousedown 事件,启动计时器;用户松开按钮时调用 mouseout 事件。
如果 mouseup 事件 2 秒内被触发,就清除计时器,当作一个普通的点击事件
如果计时器没有在 2 秒内清除,则判定为一次长按,可以执行关联的函数。
在移动端要考虑 touchstart,touchend 事件
export default { bind: function (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); }, };
防抖
需求:
防止按钮在短时间内被多次点击,使用防抖函数限制规定时间内只能点击一次。
思路:
定义一个延迟执行的方法,如果在延迟时间内再调用该方法,则重新计算执行时间。
将时间绑定在 click 方法上。
export default { inserted: function(el,binding) { let timer; el.addEventListener("click", () => { if(timer) { clearTimeout(timer) } timer = setTimeout(() => { binding.value() },1000) }) } }
debounceClick() { console.log("就应该只触发一次"); },
限制输入框类型
开发中遇到的表单输入,往往会有对输入内容的限制,比如不能输入表情和特殊字符,只能输入数字或字母等。
我们常规方法是在每一个表单的 on-change 事件上做处理。这样代码量比较大而且不好维护
需求:
根据正则表达式,设计自定义处理表单输入规则的指令,下面以禁止输入表情和特殊字符为例。
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); }; export default { bind: function (el, binding, vnode) { // 正则规则可根据需求自定义 // var regRule = /[^u4E00-u9FA5|d|a-zA-Z|rns,.?!,。?!…—&$=()-+/*{}[]]|s/g; var regRule = /[^u4E00-u9FA5dA-Za-z]|/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); }, };
使用
图片懒加载
需求:实现一个图片懒加载指令,只加载浏览器可见区域的图片。
思路:
图片懒加载的原理主要是判断当前图片是否到了可视区域这一核心逻辑实现的
拿到所有的图片 Dom ,遍历每个图片判断当前图片是否到了可视区范围内
如果到了就设置图片的 src 属性,否则显示默认图片
图片懒加载有两种方式可以实现,一是绑定 srcoll 事件进行监听,二是使用 IntersectionObserver 判断图片是否到了可视区域,
但是有浏览器兼容性问题。
下面封装一个懒加载指令兼容两种方法,判断浏览器是否支持 IntersectionObserver API,
如果支持就使用 IntersectionObserver 实现懒加载,否则则使用 srcoll 事件监听 + 节流的方法实现。
使用时: 将组件内 标签的 src 换成 v-LazyLoad
export default { install(Vue, options) { const defaultSrc = options.default; Vue.directive("lazy", { 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); }); }, // 加载真实图片 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) { 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(function () { prevTime = Date.now(); timer = null; fn.apply(context, args); }, delay); }; }, };
页面内指定容器的权限判断
背景:
在一些后台管理系统,我们可能需要根据用户角色进行一些操作权限的判断,
很多时候我们都是粗暴地给一个元素添加 v-if / v-show 来进行显示隐藏,
但如果判断条件繁琐且多个地方需要判断,这种方式的代码不仅不优雅而且冗余。
针对这种情况,我们可以通过全局自定义指令来处理。
需求:自定义一个权限指令,对需要权限判断的 Dom 进行显示隐藏。
思路:
自定义一个权限数组
判断用户的权限是否在这个数组内,如果是则显示,否则则移除 Dom
function checkArray(key) { let arr = ["1", "2", "3", "4"]; let index = arr.indexOf(key); if (index > -1) { return true; // 有权限 } else { return false; // 无权限 } } export default { 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); } } }, };
使用时:示例