vue3-directive自定义指令

208 阅读4分钟

image.png

本地自定义指令

多生命周期写法
父组件
<template>
  <div>
    <div class="box">index组件</div>
    <div>show:{{ show }}</div>
    <t-button @click="show = !show">切换</t-button>
    <hr />
    <c-vue v-move:menffy.xxx="{ backgroundColor: 'red' }" />
    <!--   v-if="show"会触发2个卸载生命周期  -->
    <!--    <c-vue v-if="show" v-move:menffy.xxx="{ backgroundColor: 'red' }" />-->
  </div>
</template>

<script lang="ts">
export default {
  name: 'ApplyAWSIndex',
};
</script>
<script setup lang="ts">
import { ref, Directive, DirectiveBinding } from 'vue';
import CVue from './C.vue';

const show = ref<boolean>(true);

interface Dir {
  backgroundColor: string;
}

// 本地自定义指令,注意命名格式,必须是:'vName'
const vMove: Directive = {
  created() {
    console.log('=====> created');
  },
  beforeMount() {
    console.log('=====> beforeMount');
  },
  // 常用的生命周期
  mounted(el: HTMLElement, dir: DirectiveBinding<Dir>, ...args) {
    console.log('=====> mounted');
    // 第一个参数是dom元素
    // 第二个是包含诸多属性的dir
    console.log('el(当前指令绑定的元素):', el);
    console.log('dir(指定绑定的传递的内容都在这 ):', dir);
    console.log('args(当前组件的虚拟dom:vnode & 上一个虚拟dom ),用的不多:', args);
    el.style.backgroundColor = dir.value.backgroundColor;
  },
  beforeUpdate() {
    console.log('=====> beforeUpdate');
  },
  // 常用的生命周期
  updated(...args) {
    console.log('=====> updated', args);
  },
  beforeUnmount() {
    console.log('=====> beforeUnmount');
  },
  // 常用的生命周期
  unmounted() {
    console.log('=====> unmounted');
    console.log('=====> 比如使用v-if隐藏组件时,会触发');
  },
};
</script>

<style scoped lang="less">
.box {
  background-color: yellow;
}
</style>
子组件
<template>
  <div style="border: 1px solid black; height: 500px">我是C</div>
</template>

<script setup lang="ts"></script>

<style scoped></style>

image.png

函数简写
  • 如果想在mounted和updated时触发相同的行为,而不关心其他的钩子函数,就可以使用函数简写
父组件
<template>
  <div>
    <div class="box">index组件</div>
    <hr />
    <div class="divC"><t-button v-show-role="`indexId:create`">创建</t-button></div>
    <div class="divC"><t-button v-show-role="`indexId:update`">更新</t-button></div>
    <div class="divC"><t-button v-show-role="`indexId:delete`">删除</t-button></div>
  </div>
</template>

<script lang="ts">
export default {
  name: 'ApplyAWSIndex',
};
</script>
<script setup lang="ts">
import { ref, Directive } from 'vue';

// 模拟用户存储的id,一般登陆页面后都能得到
localStorage.setItem('userId', 'menffy');

// 模拟后台返回的权限列表,以判断哪些按钮可以展示
const permission = [
  'menffy:indexId:create',
  'menffy:indexId:update',
  'menffy:indexId:delete'
];

// 函数简写
// 如果想在mounted和updated时触发相同的行为,而不关心其他的钩子函数,就可以使用函数简写

// Directive接收泛型,指定第一个参数类型是HTMLElement,第二个参数其内部属性value的类型为string
const vShowRole: Directive<HTMLElement, string> = (el, binding) => {
  console.log('el', el);
  console.log('binding', binding);
  const userId = localStorage.getItem('userId');
  if (!permission.includes(`${userId}:${binding.value}`)) {
    el.style.display = 'none';
  }
};
</script>

<style scoped lang="less">
.box {
  background-color: yellow;
}
.divC {
  margin-bottom: 5px;
}
</style>

image.png

指令实现弹窗可拖拽案例

<template>
  <div v-move class="box">
    <div style="position: absolute; top: 0; left: 0; background-color: green">点击拖动</div>
    <!--    上方"点击拖动"标签的position:absolute是以v-move绑定的元素做的相对定位,因为它有position:fixed-->
    <div class="content"><span style="margin-left: 120px">这里可以做成插槽</span></div>
  </div>
</template>

<script lang="ts">
export default {
  name: 'ApplyAWSIndex',
};
</script>
<script setup lang="ts">
import { Directive } from 'vue';

const vMove: Directive<HTMLElement, any> = (el, binding) => {
  // v-move绑定的元素必须有这个样式,否则无法移动
  // 要么在这里加,要么在dom元素的style里加
  el.style.position = 'fixed';

  // 指定可“点击拖动”的dom元素
  const moveEl = el.firstChild;
  
  // 根据页面布局选择参照dom元素
  // 左侧菜单
  const minWidth = document.querySelector('.t-layout__sider').clientWidth;
  // 整个可视区域的宽度 减去 可移动dom元素自身的宽度
  const maxWidth = document.querySelector('.light').clientWidth - el.clientWidth;
  // 整个可视区域的高度 减去 可移动dom元素自身的高度
  const maxHeight = document.querySelector('.t-layout').clientHeight - el.clientHeight;
  
  const mouseDown = (e: MouseEvent) => {
    const x = e.clientX - el.offsetLeft;
    const y = e.clientY - el.offsetTop;
    const mouseMove = (e: MouseEvent) => {
      const mx = e.clientX - x;
      let left = mx < minWidth ? minWidth : mx;
      left = left > maxWidth ? maxWidth : left;

      const my = e.clientY - y;
      let top = my < 0 ? 0 : my;
      top = top > maxHeight ? maxHeight : top;

      el.style.left = `${left}px`;
      el.style.top = `${top}px`;
    };
    document.addEventListener('mousemove', mouseMove);
    document.addEventListener('mouseup', () => {
      document.removeEventListener('mousemove', mouseMove);
    });
    return false;
  };
  moveEl.addEventListener('mousedown', mouseDown);
};
</script>

<style scoped lang="less">
.box {
  border: 3px solid black;
  height: auto;
  width: auto;
}
.content {
  height: 300px;
  width: 200px;
  background-color: red;
}
</style>

image.png

注册全局指令

image.png

main.ts
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

// 注册自定义指令
import { setupDirectives } from './directives';
setupDirectives(app);

app.mount('#app');
directives/index.ts
import { App } from 'vue';
import { hasRole } from './role';
import { vMove } from './moveElement';

export function setupDirectives(app: App) {
  // 权限控制指令
  app.directive('role', hasRole);
  app.directive('move', vMove);
}
directives/moveElement.ts
import { Directive } from 'vue';

export const vMove: Directive<HTMLElement, any> = (el) => {
  // v-move绑定元素必须有position=fixed这个样式,否则无法移动
  // 要么在这里加,要么在元素的style里加
  el.style.position = 'fixed';

  // 在它下方创建一个触发拖动的标签
  const moveEl = document.createElement('div');
  moveEl.innerText = '点击拖动';
  moveEl.style.position = 'absolute';
  moveEl.style.top = '0px';
  moveEl.style.left = '0px';
  el.appendChild(moveEl);

  // 根据页面布局选择参照dom元素,允许元素身体部分移出可视区域
  const minWidth = document.querySelector('.t-layout__sider').clientWidth;
  const maxWidth = document.querySelector('.light').clientWidth - el.clientWidth / 2;
  const maxHeight = document.querySelector('.t-layout').clientHeight - el.clientHeight / 3;
  const mouseDown = (e: MouseEvent) => {
    const x = e.clientX - el.offsetLeft;
    const y = e.clientY - el.offsetTop;
    const mouseMove = (e: MouseEvent) => {
      const mx = e.clientX - x;
      let left = mx < minWidth ? minWidth : mx;
      left = left > maxWidth ? maxWidth : left;

      const my = e.clientY - y;
      let top = my < 0 ? 0 : my;
      top = top > maxHeight ? maxHeight : top;

      console.log('left', left);
      console.log('top', top);
      console.log('maxWidth', maxWidth);
      console.log('maxHeight', maxHeight);

      el.style.left = `${left}px`;
      el.style.top = `${top}px`;
    };
    document.addEventListener('mousemove', mouseMove);
    document.addEventListener('mouseup', () => {
      document.removeEventListener('mousemove', mouseMove);
    });
    return false;
  };
  moveEl.addEventListener('mousedown', mouseDown);
};
DVue.vue 使用
<template>
  <!--  注册为全局指令了,可直接使用,注意要另开一个div标签,包裹需要移动的内容,比如dialog弹窗-->
  <div v-move>
    <div class="content"><span style="margin-left: 120px">各种内容</span></div>
  </div>
</template>

<script lang="ts">
export default {
  name: 'ApplyAWSIndex',
};
</script>
<script setup lang="ts"></script>

<style scoped lang="less">
.content {
  height: 300px;
  width: 200px;
  background-color: red;
}
</style>

image.png

图片懒加载

<template>
  <div style="border: 1px solid black; height: 300px; background-color: white; overflow-y: scroll">
    我是F
    <img
      v-for="item in images"
      :key="item"
      v-lazy="item"
      style="border: 1px solid black; width: 200px; height: 200px"
    />
  </div>
</template>

<script setup lang="ts">
import { Directive } from 'vue';

// 静态批量导入模块的方法。import.meta.glob则是动态导入
const imageList: Record<string, { default: string }> = import.meta.globEager('./images/*');

// 拿到图片地址列表
const images = Object.values(imageList).map((item) => item.default);

const vLazy: Directive<HTMLImageElement, string> = async (el, binding) => {
  // 动态导入,类型是Module
  const def = await import('./images/1.webp');
  console.log(def);
  // 设置未加载时的默认展示图片
  el.src = def.default;

  // vue提供的,监控dom元素出现在可视区域的相关信息,包括出现在可视区域的比例
  const obs = new IntersectionObserver((enr) => {
    const t = enr[0];
    console.log(binding.value, t);
    // dom出现在可视区域的比例
    if (t.intersectionRatio > 0) {
      setTimeout(() => {
        el.src = binding.value;
      }, 2000);
      // 关闭监控
      obs.unobserve(el);
    }
  });
  // 开始监控,参数是dom元素
  obs.observe(el);
};
</script>

<style scoped></style>

image.png