一、前言
除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令。注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。
二、如何自定义指令
1. 新建 directives 目录
在 src 目录下新建目录 directives ,用于存放自定义指令。
2. 新建自定义指令的 js 文件
在 directives 目录下新建自定义指令,例如新建 copy.js 。
3. 新建 index.js 文件
在 directives 目录下新建 index.js 文件,作为自定义指令的入口文件。
// src/directives/index.js// 引入自定义指令import copy from './copy';const directives = {copy,// ...};export default {install(Vue) {Object.keys(directives).forEach(key => {Vue.directive(key, directives[key]);});}};
4. 在 main.js 中挂载
在 main.js 中增加以下代码。
// src/main.js// 引入并挂载自定义指令import Directives from '@/directives';Vue.use(Directives);
5. 在页面中使用
// xxx.vue<template><div><button v-copy="'hello world'">复制</button></div></template>
三、自定义指令合集
1. 样式相关
元素点击范围扩展 v-expandClick
方法描述:隐式的扩展元素的点击范围,借用伪元素实现,不会影响元素在页面上的排列布局。
/*** 参数:上,右,下,左(均为可选)* 默认:10,10,10,10* 🌰 <div v-expandClick="20,30,40,50">点击范围扩大</div>*/export default function (el, binding) {const s = document.styleSheets[document.styleSheets.length - 1];const DEFAULT = -10; // 默认向外扩展 10pxconst [top, right, bottom, left] = binding.expression && binding.expression.split(',') || [];const ruleStr = `content:"";position:absolute;top:-${top || DEFAULT}px;bottom:-${bottom || DEFAULT}px;right:-${right || DEFAULT}px;left:-${left || DEFAULT}px;`;const classNameList = el.className.split(' ');el.className = classNameList.includes('expand_click_range') ? classNameList.join(' ') : [...classNameList, 'expand_click_range'].join(' ');el.style.position = el.style.position || "relative";if (s.insertRule) {s.insertRule('.expand_click_range::before' + '{' + ruleStr + '}', s.cssRules.length);} else { /* IE */s.addRule('.expand_click_range::before', ruleStr, -1);}};
2. 功能相关
文本内容复制 v-copy
方法描述:实现一键复制文本内容,用于鼠标右键粘贴。
/*** 🌰 <button v-copy="'hello world'">全屏</button>*/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);},};
全屏 v-screenfull
方法描述:点击元素进入全屏/退出全屏。可选元素后面是否插入
element-ui的全屏图标el-icon-full-screen。 ```javascript /**- 🌰 */ import screenfull from ‘screenfull’;
export default { bind (el, binding) { if (binding.modifiers.icon) { if (el.hasIcon) return; // 创建全屏图标 const iconElement = document.createElement(‘i’); iconElement.setAttribute(‘class’, ‘el-icon-full-screen’); iconElement.setAttribute(‘style’, ‘margin-left:5px’); el.appendChild(iconElement); el.hasIcon = true; } el.style.cursor = el.style.cursor || ‘pointer’; // 监听点击全屏事件 el.addEventListener(‘click’, () => handleClick()); } }
function handleClick () { if (!screenfull.isEnabled) { alert(‘浏览器不支持全屏’); return; } screenfull.toggle(); };
<a name="IHxJx"></a>### 回到顶部 v-backtop```javascriptexport default {bind(el, binding, vnode) {// 响应点击后滚动到元素顶部el.addEventListener('click', () => {const target = binding.arg ? document.getElementById(binding.arg) : window;target.scrollTo({ top: 0, behavior: 'smooth' });});},update(el, binding, vnode) {// 滚动到达参数值才出现绑定指令的元素const target = binding.arg ? document.getElementById(binding.arg) : window;if (binding.value) {target.addEventListener('scroll', (e) => {if (e.srcElement.scrollTop > binding.value) {el.style.visibility = 'unset';} else {el.style.visibility = 'hidden';}});}// 判断初始化状态if (target.scrollTop < binding.value) {el.style.visibility = 'hidden';}},unbind(el) {const target = binding.arg ? document.getElementById(binding.arg) : window;target.removeEventListener('scroll');el.removeEventListener('click');}};
拖拽 v-draggable
export default {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;}}}};
长按 v-longpress
export default {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);}};
防抖 v-debounce
export default {inserted: function (el, binding) {let timer;el.addEventListener('keyup', () => {if (timer) {clearTimeout(timer);}timer = setTimeout(() => {binding.value();}, 1000);})}};
懒加载 v-LazyLoad
export default {// install方法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 监听elobserve(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(function () {prevTime = Date.now();timer = null;fn.apply(context, args);}, delay);}}};
权限判断 v-permission
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: function (el, binding) {let permission = binding.value; // 获取到 v-permission 的值if (permission) {let hasPermission = checkArray(permission);if (!hasPermission) {// 没有权限 移除 DOM 元素el.parentNode && el.parentNode.removeChild(el);}}}};export default permission;
水印 v-waterMarker
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: function (el, binding) {addWaterMarker(binding.value.text, el, binding.value.font, binding.value.textColor);}};export default waterMarker;
