一、前言
除了核心功能默认内置的指令 (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; // 默认向外扩展 10px
const [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
```javascript
export 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 监听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(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;