Vue 在除了提供默认的十几个内置的指令外,还允许开发人员根据实际情况自定义指令,那我们在何时使用它呢?

在Vue的项目中,大多数情况下,你都可以操作数据来修改视图,也就是所谓的操作DOM,但是还是避免不了偶尔要操作原生DOM,当我们需要操作DOM的时候,就可以使用到自定义指令。

当然也能用 $refs,在未学习自定义指令之前,我们让一个想让页面的输入框自动聚焦,我们可能会怎么做:

<template>
   <input ref="input" />
</template>

<script>
  export default {
    mounted() {
      this.$refs.input.focus();
    }
  }
</script>
复制代码

上面的代码基本能实现我们需要的功能,但是要是有很多页面都需要这个功能,那我们就只能是复制这段代码过去了,而通过自定义指令我们就能回避这种问题,下面就看看如果使用指令,应该怎么做。

Vue.directive('focus', {
  bind() {},
  inserted(el) {
    el.focus()
  },
  update() {},
  componentUpdated() {},
  unbind() {}
})
复制代码

我们通过全局的Vue实例注册一个自定义指令,然后通过 v-focus 绑定到需要聚焦的 input 元素上。如果,其他组件或模块也需要聚焦功能,只要简单的绑定此指令即可。

<template>
   <input v-focus />
</template>
复制代码

自定义指令能给我们带来极高的便利,而在 Vue2 中给一个指令定义对象可以提供 bindinsertedupdatecomponentUpdatedunbind 五个钩子函数。更多详情

但是在新发布的 Vue3 中对指令也做了一些改造,主要就是对其中的钩子函数进行了优化升级,还有一些小的注意点,下面我们就一起来看看。

Vue3中指令的变化

Vue3 对指令的生命周期钩子改造了一翻,使其更像和普通组件钩子一般,更加方便可读和记忆。

app.directive('directiveName', {
    // 在绑定元素的 attribute 或事件监听器被应用之前调用, 在指令需要附加须要在普通的 v-on 事件监听器前调用的事件监听器时,这很有用
    created() {},
    // 当指令第一次绑定到元素并且在挂载父组件之前调用
    beforeMount() {},
    // 在绑定元素的父组件被挂载后调用
    mounted() {},
    // 在更新包含组件的 VNode 之前调用
    beforeUpdate() {},
    // 在包含组件的 VNode 及其子组件的 VNode 更新后调用
    updated() {},
    // 在卸载绑定元素的父组件之前调用
    beforeUnmount() {},
    // 当指令与元素解除绑定且父组件已卸载时, 只调用一次
    unmounted() {},
});
复制代码

Vue3 改造后的生命周期钩子变成了七个,而且名称变得比较好记了。

Vue3 Vue2
created  
beforeMount bind
mounted inserted
beforeUpdate update被移除!
updated componentUpdated
beforeUnmount  
unmounted unbind

注意点

Vue3 中开始支持Fragment,也就是说,我们可以在一个组件中保留多个根节点。

// HelloWorld.vue
<template>
  <div>Hello</div>
  <div>World</div>
</template>
复制代码

这会要是在一个多根组件上使用自定义指令,指令会被忽略,并且会抛出一个警告。

image.png

如果只是单根组件上使用自定义指令依旧和 Vue2 一样指令会应用在最外层节点上。

常见指令

v-copy

对于Web端来说要实现复制内容到剪贴板,一般我们都会直接选择下一个npm依赖来使用,非常方便简单。而与Vue相关的插件,Vue2有vue-clipboard2,Vue3有它的升级版vue-clipboard3

但这次我们用原生方法来写,保证这个指令不用依赖其他包,而其中最重要的一个方法就是document.execCommand('Copy'),其作用就是将拷贝当前选中内容到剪贴板。

查了一下,这个API兼容性还行。

image.png

但看了一下 MDN 文档记载竟然是个废弃的API,但现在依旧能在各大浏览器上跑,还没删除,只是没形成标准。

这本来是 IE 的私有 API,在 IE9 时被引入,后续的若干年里陆续被 Chrome / Firefix / Opera 等浏览器也做了兼容支持,但始终没有形成标准。

不过不用慌,只要还没删,我们就用它,到时有问题到时再说,哈哈哈。

在写正式代码前,我们先缕缕过程先:

  • 首先,使用场景可能是我们点击某一个按钮,就复制了某个内容(目标内容)到剪贴板中了,通过 ctrl+v 能粘贴出来。
  • 把内容塞进剪贴板,我们会用到上面提到的document.execCommand('Copy')API来实现,但是这里要注意,该API的作用是将当前 选中 的内容拷贝进剪贴板,所以我们必须让我们的目标内容被选中,才能调用该API来完成功能。
  • 让内容被选中我们能通过 HTML事件 中的 onselect() 方法来实现,而 <input /> 标签、<textarea /> 标签都能支持该事件。
  • 所以我们需要动态创建一个 <textarea /> 标签,当然该标签只是个辅助工具,所以要把它移出可视区域外。
  • 再将我们的目标内容赋值给它的value属性,将它插入到页面DOM结构中。
  • 调用 <textarea /> 标签的 onselect() 选中值,再通过 document.execCommand('Copy') API把内容复制进剪贴板。
  • 最后移除 <textarea /> 标签就可以。

具体代码就如下:

app.directive('copy', {
  beforeMount(el, binding) {
    el.targetContent = binding.value;
    el.addEventListener('click', () => {
      if(!el.targetContent) return console.warn('没有需要复制的目标内容');
      // 创建textarea标签
      const textarea = document.createElement('textarea');
      // 设置相关属性
      textarea.readOnly = 'readonly';
      textarea.style.position = 'fixed';
      textarea.style.top = '-99999px';
      // 把目标内容赋值给它的value属性
      textarea.value = el.targetContent;
      // 插入到页面
      document.body.appendChild(textarea);
      // 调用onselect()方法
      textarea.select();
      // 把目标内容复制进剪贴板, 该API会返回一个Boolean
      const res = document.execCommand('Copy');
      res && console.log('复制成功,剪贴板内容:' + el.targetContent);
      // 移除textarea标签
      document.body.removeChild(textarea);
    })
  }, 
  updated(el, binding) {
    // 实时更新最新的目标内容
    el.targetContent = binding.value;
  },
  unmounted(el) {  
    el.removeEventListener('click', ()=>{})
  }
})
复制代码

有时我们在点击复制后,可能需要个回调方法去做其他骚操作,那我们再改造改造代码。

app.directive('copy', {
  beforeMount(el, binding) {
    el.targetContent = binding.value;
    const success = binding.arg;
    el.addEventListener('click', () => {
      ...
      res && success ? success(el.targetContent) : console.log('复制成功,剪贴板内容:' + el.targetContent);
      ...
    })
  }, 
  ...
})