一、前言

除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令。注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。

注:本文中的代码均来源于网络,由笔者整理,仅用于学习参考。

二、如何自定义指令

1. 新建 directives 目录

src 目录下新建目录 directives ,用于存放自定义指令。

2. 新建自定义指令的 js 文件

directives 目录下新建自定义指令,例如新建 copy.js

3. 新建 index.js 文件

directives 目录下新建 index.js 文件,作为自定义指令的入口文件。

  1. // src/directives/index.js
  2. // 引入自定义指令
  3. import copy from './copy';
  4. const directives = {
  5. copy,
  6. // ...
  7. };
  8. export default {
  9. install(Vue) {
  10. Object.keys(directives).forEach(key => {
  11. Vue.directive(key, directives[key]);
  12. });
  13. }
  14. };

4. 在 main.js 中挂载

main.js 中增加以下代码。

  1. // src/main.js
  2. // 引入并挂载自定义指令
  3. import Directives from '@/directives';
  4. Vue.use(Directives);

5. 在页面中使用

  1. // xxx.vue
  2. <template>
  3. <div>
  4. <button v-copy="'hello world'">复制</button>
  5. </div>
  6. </template>

三、自定义指令合集

1. 样式相关

元素点击范围扩展 v-expandClick

  • 方法描述:隐式的扩展元素的点击范围,借用伪元素实现,不会影响元素在页面上的排列布局。

    1. /**
    2. * 参数:上,右,下,左(均为可选)
    3. * 默认:10,10,10,10
    4. * 🌰 <div v-expandClick="20,30,40,50">点击范围扩大</div>
    5. */
    6. export default function (el, binding) {
    7. const s = document.styleSheets[document.styleSheets.length - 1];
    8. const DEFAULT = -10; // 默认向外扩展 10px
    9. const [top, right, bottom, left] = binding.expression && binding.expression.split(',') || [];
    10. const ruleStr = `content:"";position:absolute;top:-${top || DEFAULT}px;bottom:-${bottom || DEFAULT}px;right:-${right || DEFAULT}px;left:-${left || DEFAULT}px;`;
    11. const classNameList = el.className.split(' ');
    12. el.className = classNameList.includes('expand_click_range') ? classNameList.join(' ') : [...classNameList, 'expand_click_range'].join(' ');
    13. el.style.position = el.style.position || "relative";
    14. if (s.insertRule) {
    15. s.insertRule('.expand_click_range::before' + '{' + ruleStr + '}', s.cssRules.length);
    16. } else { /* IE */
    17. s.addRule('.expand_click_range::before', ruleStr, -1);
    18. }
    19. };

    2. 功能相关

    文本内容复制 v-copy

  • 方法描述:实现一键复制文本内容,用于鼠标右键粘贴。

    1. /**
    2. * 🌰 <button v-copy="'hello world'">全屏</button>
    3. */
    4. export default {
    5. bind(el, { value }) {
    6. el.$value = value;
    7. el.handler = () => {
    8. if (!el.$value) {
    9. // 值为空的时候,给出提示。可根据项目UI仔细设计
    10. console.log('无复制内容');
    11. return;
    12. }
    13. // 动态创建 textarea 标签
    14. const textarea = document.createElement('textarea');
    15. // 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
    16. textarea.readOnly = 'readonly';
    17. textarea.style.position = 'absolute';
    18. textarea.style.left = '-9999px';
    19. // 将要 copy 的值赋给 textarea 标签的 value 属性
    20. textarea.value = el.$value;
    21. // 将 textarea 插入到 body 中
    22. document.body.appendChild(textarea);
    23. // 选中值并复制
    24. textarea.select();
    25. const result = document.execCommand('Copy');
    26. if (result) {
    27. console.log('复制成功') // 可根据项目UI仔细设计
    28. }
    29. document.body.removeChild(textarea);
    30. }
    31. // 绑定点击事件,就是所谓的一键 copy 啦
    32. el.addEventListener('click', el.handler);
    33. },
    34. // 当传进来的值更新的时候触发
    35. componentUpdated(el, { value }) {
    36. el.$value = value;
    37. },
    38. // 指令与元素解绑的时候,移除事件绑定
    39. unbind(el) {
    40. el.removeEventListener('click', el.handler);
    41. },
    42. };

    全屏 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(); };

  1. <a name="IHxJx"></a>
  2. ### 回到顶部 v-backtop
  3. ```javascript
  4. export default {
  5. bind(el, binding, vnode) {
  6. // 响应点击后滚动到元素顶部
  7. el.addEventListener('click', () => {
  8. const target = binding.arg ? document.getElementById(binding.arg) : window;
  9. target.scrollTo({ top: 0, behavior: 'smooth' });
  10. });
  11. },
  12. update(el, binding, vnode) {
  13. // 滚动到达参数值才出现绑定指令的元素
  14. const target = binding.arg ? document.getElementById(binding.arg) : window;
  15. if (binding.value) {
  16. target.addEventListener('scroll', (e) => {
  17. if (e.srcElement.scrollTop > binding.value) {
  18. el.style.visibility = 'unset';
  19. } else {
  20. el.style.visibility = 'hidden';
  21. }
  22. });
  23. }
  24. // 判断初始化状态
  25. if (target.scrollTop < binding.value) {
  26. el.style.visibility = 'hidden';
  27. }
  28. },
  29. unbind(el) {
  30. const target = binding.arg ? document.getElementById(binding.arg) : window;
  31. target.removeEventListener('scroll');
  32. el.removeEventListener('click');
  33. }
  34. };

拖拽 v-draggable

  1. export default {
  2. inserted: function (el) {
  3. el.style.cursor = 'move';
  4. el.onmousedown = function (e) {
  5. let disx = e.pageX - el.offsetLeft;
  6. let disy = e.pageY - el.offsetTop;
  7. document.onmousemove = function (e) {
  8. let x = e.pageX - disx;
  9. let y = e.pageY - disy;
  10. let maxX = document.body.clientWidth - parseInt(window.getComputedStyle(el).width);
  11. let maxY = document.body.clientHeight - parseInt(window.getComputedStyle(el).height);
  12. if (x < 0) {
  13. x = 0;
  14. } else if (x > maxX) {
  15. x = maxX;
  16. }
  17. if (y < 0) {
  18. y = 0;
  19. } else if (y > maxY) {
  20. y = maxY;
  21. }
  22. el.style.left = x + 'px';
  23. el.style.top = y + 'px';
  24. }
  25. document.onmouseup = function () {
  26. document.onmousemove = document.onmouseup = null;
  27. }
  28. }
  29. }
  30. };

长按 v-longpress

  1. export default {
  2. bind: function (el, binding, vNode) {
  3. if (typeof binding.value !== 'function') {
  4. throw 'callback must be a function';
  5. }
  6. // 定义变量
  7. let pressTimer = null
  8. // 创建计时器(2秒后执行函数)
  9. let start = (e) => {
  10. if (e.type === 'click' && e.button !== 0) {
  11. return;
  12. }
  13. if (pressTimer === null) {
  14. pressTimer = setTimeout(() => {
  15. handler();
  16. }, 2000);
  17. }
  18. }
  19. // 取消计时器
  20. let cancel = (e) => {
  21. if (pressTimer !== null) {
  22. clearTimeout(pressTimer);
  23. pressTimer = null;
  24. }
  25. }
  26. // 运行函数
  27. const handler = (e) => binding.value(e);
  28. // 添加事件监听器
  29. el.addEventListener('mousedown', start);
  30. el.addEventListener('touchstart', start);
  31. // 取消计时器
  32. el.addEventListener('click', cancel);
  33. el.addEventListener('mouseout', cancel);
  34. el.addEventListener('touchend', cancel);
  35. el.addEventListener('touchcancel', cancel);
  36. },
  37. // 当传进来的值更新的时候触发
  38. componentUpdated(el, { value }) {
  39. el.$value = value;
  40. },
  41. // 指令与元素解绑的时候,移除事件绑定
  42. unbind(el) {
  43. el.removeEventListener('click', el.handler);
  44. }
  45. };

防抖 v-debounce

  1. export default {
  2. inserted: function (el, binding) {
  3. let timer;
  4. el.addEventListener('keyup', () => {
  5. if (timer) {
  6. clearTimeout(timer);
  7. }
  8. timer = setTimeout(() => {
  9. binding.value();
  10. }, 1000);
  11. })
  12. }
  13. };

懒加载 v-LazyLoad

  1. export default {
  2. // install方法
  3. install(Vue, options) {
  4. const defaultSrc = options.default;
  5. Vue.directive('lazy', {
  6. bind(el, binding) {
  7. LazyLoad.init(el, binding.value, defaultSrc);
  8. },
  9. inserted(el) {
  10. if (IntersectionObserver) {
  11. LazyLoad.observe(el);
  12. } else {
  13. LazyLoad.listenerScroll(el);
  14. }
  15. }
  16. })
  17. },
  18. // 初始化
  19. init(el, val, def) {
  20. el.setAttribute('data-src', val);
  21. el.setAttribute('src', def);
  22. },
  23. // 利用 IntersectionObserver 监听el
  24. observe(el) {
  25. var io = new IntersectionObserver((entries) => {
  26. const realSrc = el.dataset.src;
  27. if (entries[0].isIntersecting) {
  28. if (realSrc) {
  29. el.src = realSrc;
  30. el.removeAttribute('data-src');
  31. }
  32. }
  33. })
  34. io.observe(el);
  35. },
  36. // 监听scroll事件
  37. listenerScroll(el) {
  38. const handler = LazyLoad.throttle(LazyLoad.load, 300);
  39. LazyLoad.load(el);
  40. window.addEventListener('scroll', () => handler(el));
  41. },
  42. // 加载真实图片
  43. load(el) {
  44. const windowHeight = document.documentElement.clientHeight;
  45. const elTop = el.getBoundingClientRect().top;
  46. const elBtm = el.getBoundingClientRect().bottom;
  47. const realSrc = el.dataset.src;
  48. if (elTop - windowHeight < 0 && elBtm > 0) {
  49. if (realSrc) {
  50. el.src = realSrc;
  51. el.removeAttribute('data-src');
  52. }
  53. }
  54. },
  55. // 节流
  56. throttle(fn, delay) {
  57. let timer;
  58. let prevTime;
  59. return function (...args) {
  60. const currTime = Date.now();
  61. const context = this;
  62. if (!prevTime) prevTime = currTime;
  63. clearTimeout(timer);
  64. if (currTime - prevTime > delay) {
  65. prevTime = currTime;
  66. fn.apply(context, args);
  67. clearTimeout(timer);
  68. return;
  69. }
  70. timer = setTimeout(function () {
  71. prevTime = Date.now();
  72. timer = null;
  73. fn.apply(context, args);
  74. }, delay);
  75. }
  76. }
  77. };

权限判断 v-permission

  1. function checkArray(key) {
  2. let arr = ['1', '2', '3', '4'];
  3. let index = arr.indexOf(key);
  4. if (index > -1) {
  5. return true; // 有权限
  6. } else {
  7. return false; // 无权限
  8. }
  9. };
  10. const permission = {
  11. inserted: function (el, binding) {
  12. let permission = binding.value; // 获取到 v-permission 的值
  13. if (permission) {
  14. let hasPermission = checkArray(permission);
  15. if (!hasPermission) {
  16. // 没有权限 移除 DOM 元素
  17. el.parentNode && el.parentNode.removeChild(el);
  18. }
  19. }
  20. }
  21. };
  22. export default permission;

水印 v-waterMarker

  1. function addWaterMarker(str, parentNode, font, textColor) {
  2. // 水印文字,父元素,字体,文字颜色
  3. var can = document.createElement('canvas');
  4. parentNode.appendChild(can);
  5. can.width = 200;
  6. can.height = 150;
  7. can.style.display = 'none';
  8. var cans = can.getContext('2d');
  9. cans.rotate((-20 * Math.PI) / 180);
  10. cans.font = font || '16px Microsoft JhengHei';
  11. cans.fillStyle = textColor || 'rgba(180, 180, 180, 0.3)';
  12. cans.textAlign = 'left';
  13. cans.textBaseline = 'Middle';
  14. cans.fillText(str, can.width / 10, can.height / 2);
  15. parentNode.style.backgroundImage = 'url(' + can.toDataURL('image/png') + ')';
  16. };
  17. const waterMarker = {
  18. bind: function (el, binding) {
  19. addWaterMarker(binding.value.text, el, binding.value.font, binding.value.textColor);
  20. }
  21. };
  22. export default waterMarker;

四、参考资料