vue3自定义指令

一、概述

文档
自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。其实主要就是封装作用于DOM节点的可复用逻辑

二、生命周期钩子以及钩子参数

1、生命周期

  • 与vue生命周期一样,从初始化到更新最后卸载
<script setup>
// 使用驼峰命名法
const vMyDirect = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode, prevVnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode, prevVnode) {}
}
</script>

<template>
   <div v-myDirect>我是一个div</div>
</template>
  • 简写,大多数情况下,上述生命周期钩子仅可用到mounted以及updated,故此可以继续简化为
<script setup>
// 使用驼峰命名法
const vMyDirect = (el, binding) => {
  // 这会在 `mounted` 和 `updated` 时都调用
  console.log(el, binding)
}
</script>

<template>
   <div v-myDirect>我是一个div</div>
</template>

2、钩子参数

  • el:指令绑定到的元素。这可以用于直接操作 DOM。
  • binding:一个对象,包含以下属性。
    • value:传递给指令的值。例如在 v-my-directive=“1 + 1” 中,值是 2。
    • oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 “foo”。
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }。
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode。
  • prevNode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。

三、应用

1、局部注册

  • 即组件内部注册,内部使用
  • 直接在组件内部,v开头驼峰命名,即可直接使用
<script setup>
  import { onMounted, reactive, ref, computed, watch, nextTick } from 'vue';
  // 局部自定义指令
  // https://cn.vuejs.org/guide/reusability/custom-directives.html
  defineOptions({name: ''});
  const props = defineProps({});
  const emits = defineEmits(['on-ok']);
  onMounted(() => {});

  const spanValue = ref('');
  const copyValue = ref('');
  // 使用驼峰命名法
  const vCopy = {
    created() {},
    beforeMount() {},
    // 加载
    mounted(el, binding) {
      copyValue.value = binding.value;
      const suffix = el.getAttribute('copy-value-suffix');
      el.addEventListener('click', function() {
        copyToClipboard(copyValue.value, suffix);
      });
    },
    beforeUpdate() {},
    // 更新
    updated(el, binding) {
      copyValue.value = binding.value;
    },
    // 卸载前
    beforeUnmount(el) {
      el.removeEventListener('click', function() {
        copyToClipboard();
      });
    },
    unmounted() {}
  }
  function copyToClipboard(text, suffix) {
    if(!text) {
      ElNotification.warning({
        message: '文本为空',
      })
      return;
    }
    try {
      navigator.clipboard.writeText(text + suffix)
        .then(function() {
          console.log(text.split('.'));
          ElNotification.success({
            title: '复制成功',
            message: '后缀名为' + suffix,
          })
        })
        .catch(function(err) {
          ElNotification.error({
            message: '复制失败',
          })
        });
    } catch (error) {
      ElNotification.warning({
        message: '当前浏览器不支持复制功能',
      })
    }
  }

  // 子组件暴露
  defineExpose({});

</script>

<template>
 <div>
    <el-input v-focus v-model="spanValue" style="width: 240px" placeholder="输入要复制的文本" />
    <a href="javascript:" copy-value-suffix=".exe" v-copy="spanValue">点击我进行复制</a>
  </div>
</template>

<style lang="less" scoped>
div {
  cursor: pointer;
}
</style>

2、全局注册

(1)、单独注册
  • 直接在main.js内,通过app.directive注册后即可在全局内任何模块内使用
import './assets/main.css'

import { createApp } from 'vue'
import { createPinia } from 'pinia'

import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

const app = createApp(App)

// 全局注册指令,模式一:单个注册
// 自动聚焦(v-focus)
app.directive('focus',{
  mounted: (el) => {
    setTimeout(() => {
      // el.focus();
      el.querySelector('input').focus();
    }, 200);
  }
})
// 防抖(v-debounce)
app.directive('debounce', {
  mounted(el, binding) {
    // 没有绑定函数抛出错误
    if (!(binding.value instanceof Function)) {
      throw '未绑定回调函数'
    }
    let timer
    el.addEventListener('click', () => {
      if (timer) clearTimeout(timer)
      timer = setTimeout(_ => {
        binding.value()
      }, 1000)
    })
  },
  beforeUnmount(el, binding) {
    // 一次性将元素上的所有事件监听器移除
    for (const eventType of Object.keys(el)) {
      if (element[eventType] instanceof Array) {
        element[eventType].length = 0;
      }
    }
  },
})


app.use(createPinia()).use(ElementPlus)
app.use(router)

app.mount('#app')

(2)、 批量注册
  • 一般情况下都是在src目录下创建directive目录,创建index.js文件
  • 而后就可以批量编写指令,之后全部放在一个对象中,export default导出,最后再在main.js中引入后遍历对象,批量app.directive注册指令
  • 需要注意的是与局部注册不一样的是,通过directive注册的指令,无需以v开头的驼峰命名
// src/directive/index.js
const debounce = {
  mounted(el, binding) {
    // 没有绑定函数抛出错误
    if (!(binding.value instanceof Function)) {
      throw '未绑定回调函数'
    }
    let timer
    el.addEventListener('click', () => {
      if (timer) clearTimeout(timer)
      timer = setTimeout(_ => {
        binding.value()
      }, 1000)
    })
  },
  beforeUnmount(el, binding) {
    // 一次性将元素上的所有事件监听器移除
    for (const eventType of Object.keys(el)) {
      if (element[eventType] instanceof Array) {
        element[eventType].length = 0;
      }
    }
  },
}

const directive = {
  mounted: (el) => {
    setTimeout(() => {
      // el.focus();
      el.querySelector('input').focus();
    }, 200);
  }
}

export default { debounce, directive }
// main.js
import './assets/main.css'

import { createApp } from 'vue'
import { createPinia } from 'pinia'

import App from './App.vue'
import router from './router'
import project from '@/directive/index.js'

console.log('project', project);

const app = createApp(App)


// 全局注册指令,模式二:批量注册,直接引入js文件,通过object.keys()遍历,后directive注册
Object.keys(project).forEach((key) => {
  app.directive(key, project[key])
})

app.use(createPinia())
app.use(router)

app.mount('#app')

(3)、批量注册,配合插件使用
  • 配合插件可实现批量注册全局指令
    • 插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码
    • 一个插件可以是一个拥有 install() 方法的对象,也可以直接是一个安装函数本身。安装函数会接收到安装它的应用实例和传递给 app.use() 的额外选项作为参数
  • 同上创建src/directive/index.js目录,而后在directive目录内创建project/index.js目录
  • project目录内创建index.js,用于封装批量编写的指令,最后导出到directive/index.js
  • directive/index.js引入后,批量注册指令,最后导出至mian.js使用app.use安装即可
// src/directive/project/index.js
const debounce = {
  mounted(el, binding) {
    // 没有绑定函数抛出错误
    if (!(binding.value instanceof Function)) {
      throw '未绑定回调函数'
    }
    let timer
    el.addEventListener('click', () => {
      if (timer) clearTimeout(timer)
      timer = setTimeout(_ => {
        binding.value()
      }, 1000)
    })
  },
  beforeUnmount(el, binding) {
    // 一次性将元素上的所有事件监听器移除
    for (const eventType of Object.keys(el)) {
      if (element[eventType] instanceof Array) {
        element[eventType].length = 0;
      }
    }
  },
}

const directive = {
  mounted: (el) => {
    setTimeout(() => {
      // el.focus();
      el.querySelector('input').focus();
    }, 200);
  }
}

export default { debounce, directive }
// src/directive/index.js
import directive from "./project/index.js"; // 引入需要的指令

const directives = {
  // 一个插件可以是一个拥有 install() 方法的对象,也可以直接是一个安装函数本身。
  // 他有两个参数,安装它的应用实例(app)和 app.use 安装时传递的额外选项(options)
  install: function (app, options) {
    // 常用 app.component() 、 app.directive() 、app.provide() 、app.config.globalProperties
    Object.keys(directive).forEach((key) => {
      app.directive(key, directive[key]); // 注册
    });
  }
};

// 抛出,到main.js中引入,通过use注册为插件
export default directives;
// main.js
import './assets/main.css'

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import directive from '@/directive/index.js'

import App from './App.vue'
import router from './router'

// 全局注册指令,模式三:配合插件安装实现批量注册
app.use(directive, {a: '121'});

app.use(createPinia())
app.use(router)

app.mount('#app')

四、其他

  • 除了一些偏向于逻辑的指令,可否实现偏向于组件的指令(例如气泡提示,对话窗口之类的),答案是肯定的
  • 首先要先了解渲染函数 & JSX
    • 在绝大多数情况下,Vue 推荐使用模板语法来创建应用。然而在某些使用场景下,我们真的需要用到 JavaScript 完全的编程能力。这时渲染函数就派上用场了。
  • 在了解了渲染函数后,我们便可以通过js在mounted中直接将该组件创建为一个虚拟dom,然后再创建一个真实dom节点作为容器,最后再将该虚拟dom挂载到容器内,最后将该容器渲染到body中
  • src/directive/project目录下创建.vue文件,例如气泡提示组件myPopover.vue
// src/directive/project/myPopover.vue
<script setup>
import { onMounted, reactive, ref, computed, watch, nextTick, useSlots, inject } from 'vue';

defineOptions({ name: '' });
const props = defineProps({
  id: {required: true, type: String, default: ''},
  virtualRef: {required: true},
  trigger: {type: String, default: 'hover'},
  placement: {type: String, default: 'top'},
  width: {type: Number, default: 300},
  hideAfter: {type: Number, default: 300},
  showAfter: {type: Number, default: 100},
})
const emits = defineEmits(['on-ok']);
onMounted(() => {
  console.log('id', props.id);
  // console.log('myPopover', useSlots().mySlot ? true : false);
});
  const a = inject('a')
  console.log('a', a);

// 子组件暴露
defineExpose({});

const show = (event) => {
  console.log(event);
}


</script>

<template>
  <div>
    <el-popover virtual-triggering v-bind="props" @show="show">
      <div class="projectBox" ref="projectRef">
        我是气泡弹窗
      </div>
    </el-popover>
  </div>
</template>

<style lang="less" scoped></style>
  • 导出到src/directive/project/indx.js内
  • 引入createVNode、 render
  • 将组件创建一个虚拟dom节点以及一个容器节点,最后将该容器渲染到body中
// src/directive/project/indx.js
import {createVNode, render, resolveComponent} from "vue";
import myPopoverEl from './myPopover.vue'

let container;
const myPopover = {
  mounted(el, binding) {
    // 创建一个myPopoverEl虚拟节点
    const vnode = createVNode(myPopoverEl, {virtualRef: el, ...binding.value});
    // 而后创建一个容器用于渲染虚拟节点
    container = document.createElement('div');
    render(vnode, container);
    // 最后将该容器渲染到body中
    document.body.appendChild(container);
  },
  beforeUnmount() {
    // 卸载时记得将容器从body中移除
    document.body.removeChild(container);
  }
}

export default { myPopover }
  • 使用
<el-button type="primary">
  <span v-myPopover="{id: '124892749', placement: 'bottom'}">popover</span>
</el-button>

  • 24
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值