📖. 前言


使用自定义指令可以满足我们日常一些场景,这里给出几个自定义指令的案例:

一. 批量注册指令


新建 directives/index.js 文件

import copy from './copy';
import emoji from './emoji';

// 自定义指令
const directives = {
  copy,
  emoji
}
// 这种写法可以批量注册指令
export default {
  install (Vue) {
    Object.keys(directives).forEach((key) => {
      Vue.directive(key, directives[key]);
    })
  }
}

main.js 引入并调用

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

实用指令分享


1. 一键 Copy

  • 需求
    实现一键复制文本内容,用于鼠标右键粘贴。
  • 思路
    1.动态创建 textarea 标签,并设置 readOnly 属性及移出可视区域
    2.将要复制的值赋给 textarea 标签的 value 属性,并插入到 body
    3.选中值 textarea 并复制
    4.将 body 中插入的 textarea 移除
    5.在第一次调用时绑定事件,在解绑时移除事件
import { Message } from 'element-ui';

const copy = {
  /*
    bind 钩子函数,第一次绑定时调用,可以在这里做初始化设置
    el: 作用的 dom 对象
    value: 传给指令的值,也就是我们要 copy 的值
  */
  bind (el, { value }) {
    el.$value = value // 用一个全局属性来存传进来的值,因为这个值在别的钩子函数里还会用到
    el.handler = () => {
      // 值为空的时候,给出提示。
      if (!el.$value) {
        // 可根据项目 UI 仔细设计
        Message.warning('无复制内容');
        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) {
        // 可根据项目 UI 仔细设计
        Message.success('复制成功');
        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)
  }
}

export default copy

2. 懒加载

  • 背景
    在类电商类项目,往往存在大量的图片,如 banner 广告图,菜单导航图,美团等商家列表头图等。图片众多以及图片体积过大往往会影响页面加载速度,造成不良的用户体验,所以进行图片懒加载优化势在必行。
  • 需求
    实现一个图片懒加载指令,只加载浏览器可见区域的图片。
  • 思路
    1.图片懒加载的原理主要是判断当前图片是否到了可视区域这一核心逻辑实现的
    2.拿到所有的图片 Dom ,遍历每个图片判断当前图片是否到了可视区范围内
    3.如果到了就设置图片的 src 属性,否则显示默认图片

图片懒加载有两种方式可以实现,一是绑定 srcoll 事件进行监听,二是使用 IntersectionObserver 判断图片是否到了可视区域,但是有浏览器兼容性问题。

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

const LazyLoad = {
  // install 方法
  install (Vue, options) {
    let 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) {
    // data-src 储存真实 src
    el.setAttribute('data-src', val);
    // 设置 src 为 loading 图
    el.setAttribute('src', def);
  },
  // 利用 IntersectionObserver 监听 el
  observe (el) {
    var io = new IntersectionObserver(entries => {
      let realSrc = el.dataset.src;
      if (entries[0].isIntersecting) {
        if (realSrc) {
          el.src = realSrc;
          el.removeAttribute('data-src');
        }
      }
    });
    io.observe(el);
  },
  // 监听 scroll 事件
  listenerScroll (el) {
    let handler = LazyLoad.throttle(LazyLoad.load, 300);
    LazyLoad.load(el);
    window.addEventListener('scroll', () => {
      handler(el);
    });
  },
  // 加载真实图片
  load (el) {
    let windowHeight = document.documentElement.clientHeight
    let elTop = el.getBoundingClientRect().top;
    let elBtm = el.getBoundingClientRect().bottom;
    let 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) {
      let currTime = Date.now();
      let 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);
    }
  }

}

export default LazyLoad;

3. 表单防止重复提交

  • 需求
    实现一个重复提交指令,用于表单提交时。
Vue.directive('throttle', {
  bind: (el, binding) => {
    let throttleTime = binding.value; // 节流时间
    if (!throttleTime) { // 用户若不设置节流时间,则默认2s
      throttleTime = 2000;
    }
    let cbFun;
    el.addEventListener('click', event => {
      if (!cbFun) { // 第一次执行
        cbFun = setTimeout(() => {
          cbFun = null;
        }, throttleTime);
      } else {
        event && event.stopImmediatePropagation();
      }
    }, true);
  },
});
<!-- 为 button 标签设置 v-throttle 自定义指令 -->
<button @click="sayHello" v-throttle>提交</button>

关于自定义指令还有很多应用场景,如:拖拽指令、页面水印、权限校验等等应用场景