yuhengshen / vue-auth-layer

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

背景

昨天看到一篇文章,面试官问我按钮级别权限怎么控制,我说v-if,面试官说再见 - 掘金 (juejin.cn),里面介绍了三种权限校验方式,看起来挺不错。但是,考虑到对于按钮细粒度要求高的场景,如果每一个按钮权限都去设置 v-auth='xxx.xxx.xx'的话,累死人不说,还非常容易出错。

这里简单列举使用v-auth指令不方便的场景。

  1. 同一个包含权限按钮组件,放到不同的父组件中,需要父组件透传v-auth路径。
  2. 当权限按钮组件所处的环境变化后,需要先修改v-auth路径,然后再去管理中心调整对应的配置。(如按钮本来在 tab1下,v-auth值为tab1-编辑,当你的tab1名称改为tab2时,你不得不手动遍历tab1下的所有v-auth,将tab1-xxx变为tab2-xxx,过程相当繁琐且容易出错。)
  3. v-auth的值为手动输入,手动就容易文字拼错。

方案思考

我们能不能自动获取权限按钮所处的位置,然后进行自动权限判断呢?位置如何获取呢?是否可以自动provide和inject相关的权限层呢?

  • 使用指令:指令内部不能获取完整的Vue特性,无法provide和inject,所以不考虑这种实现方式。请查看相关讨论Provide/inject for custom directives · Issue #6487 · vuejs/vue (github.com)

  • 使用装饰器:这个获取到的信息比指令还少,不考虑使用

  • 使用父组件包装:这是可以的,但是当权限按钮多的时候,我们每一个按钮和权限层组件都要包装一次父组件,这对于可读性、代码量、重复prop都有不小的负担

  • 拦截按钮组件:通过对拦截组件,或者重写组件,注入对应的权限判断逻辑,自动化和控制能力都比较高

为了能自动化权限判断,减小出错可能,我这里采用的是最后一种拦截组件,注入逻辑的方案。

实现

如何判断当前权限所处的位置呢?

考虑到在后台管理中,我们往往有这么几种权限分层场景

  • 当前组件所处的路由地址(作为权限列表的名称,作为权限第一级区分方式)
  • 当前组件所处的tab名称(权限层,此时权限路径名称为 'tabName-authText')
  • 当前组件所处的弹窗名称(权限层,此时权限路径名称为 'dialogTitle-authText')

对于路由地址,我们可以在hooks处理前存储一下当前的静态路由地址(为什么不使用computed来自动获取?这是为了避免路由切换时,keep-alive重复计算),这里不做详细解释。

对于通用的权限层我们我们实现一个hook,来自动inject上层的authPath,并继续provide当前层的名称。

/**
 * @param name - 当前权限层的名称,如:Tab的名称,Dialog的title等
 */
export function useProvideAuth(name?: string) {
  if (!name) return;
  const injectName = inject("auth");

  const provideName = toRef(() =>
    unref(injectName) ? `${unref(injectName)}-${name}` : name
  );

  provide("auth", provideName);
}

然后,通过拦截导致权限分层的组件(这里是TabPane和Modal),我们在权限层使用这个hook

  1. TabPane权限层
<template>
  <NTabPane :tab="tab" :name="name">
    <slot></slot>
  </NTabPane>
</template>

<script setup lang="ts">
import { useProvideAuth } from "../hooks";
import { NTabPane } from "naive-ui";

defineOptions({
  name: "TabPane",
  // naive-ui: https://github.com/tusen-ai/naive-ui/blob/0839b0ea5e4077e260be4bc3df5d525c35066449/src/tabs/src/Tabs.tsx#L648
  __TAB_PANE__: true,
});
const props = defineProps<{
  auth?: boolean;
  tab: string;
  name: string;
}>();

useProvideAuth(props.tab);
</script>
  1. Modal权限层
<template>
  <NModal :title="title">
    <slot></slot>
  </NModal>
</template>

<script setup lang="ts">
import { NModal } from "naive-ui";
import { useProvideAuth } from "../hooks";

const props = defineProps<{
  auth?: boolean;
  title?: string;
}>();

useProvideAuth(props.title);
</script>

这样我们就支持任意层次的权限层进行互相嵌套了。

如何判断当前权限按钮的文字呢?

我们通过权限层能准确地获取当前权限按钮的位置了,那么我们如何才能获取按钮的具体文字呢?

  1. 我们这里通过遍历按钮中的slots,判断是否为文字,然后将文字和权限层提供的 authPath结合路由名称,即可完全确定一个权限的具体路径了。
  2. 我们也可以传入自定义的权限文字,对于特定场景进行定制权限(权限文字和按钮文字表现不一致时)
/**
 * 
 * @param auth - 是否进行权限管控
 * @param authText - 自定义权限按钮名称,不通过slot自动获取
 * @returns 
 */
export function useAuth(auth: MaybeRefOrGetter<boolean>, authText?: string) {
  const routeName = "routeNameExample"; // 正常通过useAuth静态传入routeName,避免切换页面时,keep-alive重复计算
  const prevAuthName = inject("auth") as Ref<string> | undefined;
  const slots = useSlots();
  const getSlotChildrenText = (
    children: VNode<
      RendererNode,
      RendererElement,
      {
        [key: string]: any;
      }
    >[]
  ) =>
    children
      .map((node): string => {
        // 这里可能有其他情况,暂时不考虑,需要的自定判断
        if (!node.children || typeof node.children === "string")
          return node.children || "";
        if (typeof (node.children as any).default === "function")
          return getSlotChildrenText((node.children as any).default());
        return "";
      })
      .join("");

  const authValue = toValue(auth);
  const defaultSlots = slots.default?.();
  if (!authText)
    authText = defaultSlots && getSlotChildrenText(defaultSlots).trim();

  const authPath = toRef(() => {
    if (!authText) return;
    return prevAuthName ? `${prevAuthName.value}-${authText}` : authText;
  });
  const show = toRef(
    () =>
      !authValue ||
      !authPath.value ||
      authConfig[routeName]?.includes(authPath.value)
  );

  return {
    show,
    authPath,
  };
}

使用这个hook为权限按钮注入判断逻辑,并且我们可以在权限不满足时,提供一些其他的辅助能力,或者提供一些自动化的能力

<template>
  <NButton v-if="show" key="1">
    <slot></slot>
  </NButton>
  <!-- 这里我们通过自定义逻辑和页面,控制权限按钮的其他逻辑,如下:当权限不满足时,显示了一个按钮来复制当前的权限路径-->
  <NButton v-else type="warning" key="2" @click="copyAuthPath">
    复制权限路径
  </NButton>
</template>

<script setup lang="ts">
import { NButton } from "naive-ui";
import { useAuth } from "../hooks";

const props = defineProps<{
  auth?: boolean;
}>();

const { show, authPath } = useAuth(() => props.auth);

const copyAuthPath = () => {
  if (authPath.value) {
    navigator.clipboard.writeText(authPath.value);
  }
};
</script>

使用

我这里使用的是naive-ui,注册好全局组件后,替换我们重写好的拦截组件

import { createApp } from "vue";
import App from "./App.vue";
import naive from "naive-ui";
import AuthTabPaneVue from "./components/AuthTabPane.vue";
import AuthButtonVue from "./components/AuthButton.vue";
import AuthModal from "./components/AuthModal.vue";

const app = createApp(App);

app.use(naive);

app.component("n-tab-pane", AuthTabPaneVue);
app.component("n-button", AuthButtonVue);
app.component("n-modal", AuthModal);

app.mount("#app");

在业务组件内,我们可以很方便的进行使用

<script setup lang="ts">
import { ref } from "vue";
const showModal = ref(false);
</script>

<template>
  <n-tabs type="line">
    <n-tab-pane name="tab1" tab="第一个Tab">
      <n-button auth> Tab中的按钮权限 </n-button>
    </n-tab-pane>
    <n-tab-pane name="tab2" tab="第二个Tab">
      <n-button @click="showModal = true">显示弹窗</n-button>
      <n-modal
        v-model:show="showModal"
        preset="dialog"
        title="Dialog"
        positive-text="Submit"
        negative-text="Cancel"
      >
        <n-button auth>Tab中Modal中的权限校验</n-button>
        <n-tabs>
          <n-tab-pane name="dialogTabPane" tab="dialog中的TabPane">
            <n-button auth>嵌套的权限</n-button>
          </n-tab-pane>
        </n-tabs>
      </n-modal>
    </n-tab-pane>
  </n-tabs>

  <n-button :auth="true" style="margin-top: 1em">普通的按钮权限</n-button>
</template>

如此就自动进行了如下权限判断。可以看业务代码,是非常简洁的,仅仅对需要权限校验的按钮增加了一个auth属性

路由名称 按钮 权限路径
routeNameExample 普通的按钮权限 普通的按钮权限
routeNameExample Tab中的按钮权限 第一个Tab-Tab中的按钮权限
routeNameExample Tab中Modal中的权限校验 第二个Tab-Dialog-Tab中Modal中的权限校验
routeNameExample 嵌套的权限 第二个Tab-Dialog-dialog中的TabPane-嵌套的权限

我们的配置文件如下:

export default {
  routeNameExample: [
    "第一个Tab-Tab中的按钮权限",
    "第二个Tab-Dialog-Tab中Modal中的权限校验",
    "普通的按钮权限",
    "第二个Tab-Dialog-dialog中的TabPane-嵌套的权限2",
  ],
};

界面展示如下:

image.png

可以看到,按钮都正常显示了,对于没有权限的按钮,显示了一个复制权限路径的按钮,这里大家可以自定义其他的逻辑,来实现一些自动化配置。

预览地址

此次实现较为简略,可能存在一些问题,这里主要提供的是一种方式,具体业务还需要考虑,如权限控制可能不止有button,还有其他的一些组件。

codesandbox

About


Languages

Language:TypeScript 51.2%Language:Vue 42.4%Language:HTML 6.4%