Vue3 权限控制指令:实现点击事件拦截

819 阅读4分钟

需求

有很多功能模块。用户等级不同可以使用的功能也不一样。但是所有模块的页面必须全部显示
只有点击的时候如果没这个功能的权益才提示去购买。并且拦截原先按钮的点击事件函数。

最终实现使用:

<template>
    <el-button @click="oldClickHandle"  v-hasService="'order'">查看订单</el-button>
</template>

思路:

思路1: 在oldClickHandle内部判断权限逻辑。
这样每个用到的点击处理函数都要修改,这种方式对代码的侵入性很强。

思路2(采用):

编写vue指令。 在指令mounted阶段增加点击事件监听器。点击的时候触发指令的监听器后。判断是否有功能权限。再决定是否停止事件继续传播和提示去购买。

问题:

编写指令后发现@click绑定的事件处理函数执行优先于指令增加的事件处理函数
这就很难受了,正常来说确实是同一个元素绑定多个事件侦听器是按照绑定的顺序执行的。
例如:

el.addEventListener('click', handler1)
el.addEventListener('click', handler2)

点击元素的话会先执行 handler1再执行handler2。
但我们希望先执行handler2在执行handler1。

原理:

这时候就要引入捕获和冒泡的知识了。 去MDN寻找答案。

先看addEventListener的一个useCapture参数

image.png

看一下Dom事件流(传播流程)图:来源

image.png

可以知道事件流向分3个阶段:

捕获阶段:事件对象通过目标的祖先从Window传播到目标的父对象。这个阶段也被称为捕获阶段。

目标阶段:事件对象到达事件对象的事件目标。这一阶段也被称为靶期。如果事件类型表明事件不冒泡,则事件对象将在此阶段完成后停止。

冒泡阶段:事件对象以相反的顺序通过目标的祖先传播,从目标的父对象开始,以Window结束。这个阶段也被称为冒泡阶段。

事件处理函数在哪个阶段被执行?

image.png

根据文档得知。默认是冒泡阶段执行。
但是如果执行addEventListener时候最后一个参数(useCaptreue)设置为true。
就会在捕获阶段执行事件处理函数

根据上面的事件流向图,可以知道捕获阶段先于冒泡阶段

解决方案自然就出来了:useCaptreue设置为true让事件处理函数在捕获阶段执行。

指令源码

附上指令代码:

import { Directive } from 'vue'

let hasCodes = ['order','system','user']

export const hasPermisson: Directive = {
  mounted(el, binding) {
    const { value } = binding
    const handler = (event: any) => { 
      // 如果没有权限
      if(!hasCodes.includes(value)){
        // 停止事件传播
        event.stopPropagation()
        // 阻止默认行为,例如组件使用到输入框、label、等。这些也拦截掉。
        event.preventDefault()
        alert('无该权限,请去开通')
      }
    }
    el.__hasPermissonHandler = handler
    // 捕获阶段执行
    el.addEventListener('click', handler, true)
  },
  unmounted(el) {
    const handler = el.__hasPermissonHandler
    if (handler) {
      el.removeEventListener('click', handler)
      delete el.__hasPermissonHandler
    }
  },
}

export const vHasPermisson: Directive = hasPermisson

注册指令:

//main.ts
import { createApp } from "vue";
import { hasService } from "@/directives/hasService";
const app = createApp(App);
app.directive("has", hasService);

使用示例:

// 模板文件
<template>
    <div @click="orderHandle" v-has="'order'">提交订单</div>
</template>

这时候,如果点击提交订单,那么会先检查权限:
如果没有order模块权限,那么就停止当前事件的继续传播,自然就拦截掉了 orderHandle(这个默认冒泡阶段才执行的处理函数) 如果有order模块权限,什么都不做。

扩展-事件委托-性能优化

**利用事件冒泡和事件委托机制可以实现性能优化: **

事件委托(Event Delegation)是指将事件的处理程序绑定到父元素上(基于事件冒泡原理),通过判断事件的原始目标(event.target)来决定是否执行相应的操作。不需要为子元素单独绑定事件处理程序,而是直接通过父元素来处理事件,这样可以减少事件绑定的数量、提高性能

委托示例代码:

<template>
  <div @click="parentHandle" style="text-align:center">
    <div v-for="item in list" :data-id="item">{{item}}</div>
  </div>
 <div>{{tip}}</div>
</template>

<script>
import { defineComponent, ref } from 'vue';

export default defineComponent({
  setup() {
    const list = ref([1,2,3,4,5,6,7,8,9,10]);
    const tip = ref('')
    const parentHandle = (event) => {
      let id = event.target.dataset.id
      id && (tip.value = `点击了id:${id}`)
    }
    return { list, parentHandle, tip };
  },
});
</script>

总结

通过addEventListener函数的第三个参数useCaptreue设置为true即可让事件处理函数优先执行。
日后有类似这种需要调整事件处理程序触发顺序需求的时候就知道解决方案了。

利用冒泡机制事件委托方式还可实现性能优化,其通过将事件处理程序绑定到祖先元素,减少事件处理程序数量和内存占用,如在列表中可将点击处理程序绑定到列表元素来处理列表项的点击。

至此,一个对原点击事件处理函数无侵入性的点击权限指令就开发完成了。