面試官問我按鈕級別權(quán)限怎么控制,我說v-if,面試官說再見
最近的面試中有一個面試官問我按鈕級別的權(quán)限怎么控制,我說直接v-if
啊,他說不夠好,我說我們項目中按鈕級別的權(quán)限控制情況不多,所以v-if
就夠了,他說不夠通用,最后他對我的評價是做過很多東西,但是都不夠深入,好吧,那今天我們就來深入深入。
因為我自己沒有相關(guān)實踐,所以接下來就從這個有16.2k
星星的后臺管理系統(tǒng)項目Vue vben admin中看看它是如何做的。
獲取權(quán)限碼
要做權(quán)限控制,肯定需要一個code
,無論是權(quán)限碼還是角色碼都可以,一般后端會一次性返回,然后全局存儲起來就可以了,Vue vben admin
是在登錄成功以后獲取并保存到全局的store
中:
js復(fù)制代碼import { defineStore } from 'pinia'; export const usePermissionStore = defineStore({ ?? ?state: () => ({ ?? ? ? ?// 權(quán)限代碼列表 ?? ? ? ?permCodeList: [], ?? ?}), ?? ?getters: { ?? ? ? ?// 獲取 ?? ? ? ?getPermCodeList(){ ?? ? ? return this.permCodeList; ?? ? }, ? ? ?}, ?? ?actions: { ?? ? ? ?// 存儲 ?? ? ? ?setPermCodeList(codeList) { ?? ? ? ? ? ?this.permCodeList = codeList; ?? ? ? ?}, ? ? ? ? ?// 請求權(quán)限碼 ?? ? ? ?async changePermissionCode() { ?? ? ? ? ? ?const codeList = await getPermCode(); ?? ? ? ? ? ?this.setPermCodeList(codeList); ?? ? ? ?} ?? ?} })
接下來它提供了三種按鈕級別的權(quán)限控制方式,一一來看。
函數(shù)方式
使用示例如下:
html復(fù)制代碼<template> ??<a-button v-if="hasPermission(['20000', '2000010'])" color="error" class="mx-4"> ?? ?擁有[20000,2000010]code可見 ??</a-button> </template> ?<script lang="ts"> ??import { usePermission } from '/@/hooks/web/usePermission'; ? ?export default defineComponent({ ?? ?setup() { ?? ? ?const { hasPermission } = usePermission(); ?? ? ?return { hasPermission }; ?? ?}, ??}); </script>
本質(zhì)上就是通過v-if
,只不過是通過一個統(tǒng)一的權(quán)限判斷方法hasPermission
:
js復(fù)制代碼export function usePermission() { ?? ?function hasPermission(value, def = true) { ?? ? ? ?// 默認視為有權(quán)限 ?? ? ? ?if (!value) { ?? ? ? ? ? ?return def; ?? ? ? ?} ? ? ? ? ?const allCodeList = permissionStore.getPermCodeList; ?? ? ? ?if (!isArray(value)) { ?? ? ? ? ? ?return allCodeList.includes(value); ?? ? ? ?} ?? ? ? ?// intersection是lodash提供的一個方法,用于返回一個所有給定數(shù)組都存在的元素組成的數(shù)組 ?? ? ? ?return (intersection(value, allCodeList)).length > 0; ? ? ? ? ?return true; ?? ?} }
很簡單,從全局store
中獲取當前用戶的權(quán)限碼列表,然后判斷其中是否存在當前按鈕需要的權(quán)限碼,如果有多個權(quán)限碼,只要滿足其中一個就可以。
組件方式
除了通過函數(shù)方式使用,也可以使用組件方式,Vue vben admin
提供了一個Authority
組件,使用示例如下:
html復(fù)制代碼<template> ??<div> ?? ?<Authority :value="RoleEnum.ADMIN"> ?? ? ?<a-button type="primary" block> 只有admin角色可見 </a-button> ?? ?</Authority> ??</div> </template> <script> ??import { Authority } from '/@/components/Authority'; ??import { defineComponent } from 'vue'; ??export default defineComponent({ ?? ?components: { Authority }, ??}); </script>
使用Authority
包裹需要權(quán)限控制的按鈕即可,該按鈕需要的權(quán)限碼通過value
屬性傳入,接下來看看Authority
組件的實現(xiàn)。
js復(fù)制代碼<script lang="ts"> ??import { defineComponent } from 'vue'; ??import { usePermission } from '/@/hooks/web/usePermission'; ??import { getSlot } from '/@/utils/helper/tsxHelper'; ? ?export default defineComponent({ ?? ?name: 'Authority', ?? ?props: { ?? ? ?value: { ?? ? ? ?type: [Number, Array, String], ?? ? ? ?default: '', ?? ? ?}, ?? ?}, ?? ?setup(props, { slots }) { ?? ? ?const { hasPermission } = usePermission(); ? ? ? ?function renderAuth() { ?? ? ? ?const { value } = props; ?? ? ? ?if (!value) { ?? ? ? ? ?return getSlot(slots); ?? ? ? ?} ?? ? ? ?return hasPermission(value) ? getSlot(slots) : null; ?? ? ?} ? ? ? ?return () => { ?? ? ? ?return renderAuth(); ?? ? ?}; ?? ?}, ??}); </script>
同樣還是使用hasPermission
方法,如果當前用戶存在按鈕需要的權(quán)限碼時就原封不動渲染Authority
包裹的內(nèi)容,否則就啥也不渲染。
指令方式
最后一種就是指令方式,使用示例如下:
js復(fù)制代碼<a-button v-auth="'1000'" type="primary" class="mx-4"> 擁有code ['1000']權(quán)限可見 </a-button>
實現(xiàn)如下:
js復(fù)制代碼import { usePermission } from '/@/hooks/web/usePermission'; ?function isAuth(el, binding) { ??const { hasPermission } = usePermission(); ? ?const value = binding.value; ??if (!value) return; ??if (!hasPermission(value)) { ?? ?el.parentNode?.removeChild(el); ??} } ?const mounted = (el, binding) => { ??isAuth(el, binding); }; ?const authDirective = { ??// 在綁定元素的父組件 ??// 及他自己的所有子節(jié)點都掛載完成后調(diào)用 ??mounted, }; ?// 注冊全局指令 export function setupPermissionDirective(app) { ??app.directive('auth', authDirective); }
只定義了一個mounted
鉤子,也就是在綁定元素掛載后調(diào)用,依舊是使用hasPermission
方法,判斷當前用戶是否存在通過指令插入的按鈕需要的權(quán)限碼,如果不存在,直接移除綁定的元素。
很明顯,Vue vben admin
的實現(xiàn)有兩個問題,一是不能動態(tài)更改按鈕的權(quán)限,二是動態(tài)更改當前用戶的權(quán)限也不會生效。
解決第一個問題很簡單,因為上述只有刪除元素的邏輯,沒有加回來的邏輯,那么增加一個updated
鉤子:
js復(fù)制代碼app.directive("auth", { ?? ?mounted: (el, binding) => { ?? ? ? ?const value = binding.value ?? ? ? ?if (!value) return ?? ? ? ?if (!hasPermission(value)) { ?? ? ? ? ? ?// 掛載的時候沒有權(quán)限把元素刪除 ?? ? ? ? ? ?removeEl(el) ?? ? ? ?} ?? ?}, ?? ?updated(el, binding) { ?? ? ? ?// 按鈕權(quán)限碼沒有變化,不做處理 ?? ? ? ?if (binding.value === binding.oldValue) return ?? ? ? ?// 判斷用戶本次和上次權(quán)限狀態(tài)是否一樣,一樣也不用做處理 ?? ? ? ?let oldHasPermission = hasPermission(binding.oldValue) ?? ? ? ?let newHasPermission = hasPermission(binding.value) ?? ? ? ?if (oldHasPermission === newHasPermission) return ?? ? ? ?// 如果變成有權(quán)限,那么把元素添加回來 ?? ? ? ?if (newHasPermission) { ?? ? ? ? ? ?addEl(el) ?? ? ? ?} else { ?? ? ? ?// 如果變成沒有權(quán)限,則把元素刪除 ?? ? ? ? ? ?removeEl(el) ?? ? ? ?} ?? ?}, }) ?const hasPermission = (value) => { ?? ?return [1, 2, 3].includes(value) } ?const removeEl = (el) => { ?? ?// 在綁定元素上存儲父級元素 ?? ?el._parentNode = el.parentNode ?? ?// 在綁定元素上存儲一個注釋節(jié)點 ?? ?el._placeholderNode = document.createComment("auth") ?? ?// 使用注釋節(jié)點來占位 ?? ?el.parentNode?.replaceChild(el._placeholderNode, el) } ?const addEl = (el) => { ?? ?// 替換掉給自己占位的注釋節(jié)點 ?? ?el._parentNode?.replaceChild(el, el._placeholderNode) }
主要就是要把父節(jié)點保存起來,不然想再添加回去的時候獲取不到原來的父節(jié)點,另外刪除的時候創(chuàng)建一個注釋節(jié)點給自己占位,這樣下次想要回去能知道自己原來在哪。
第二個問題的原因是修改了用戶權(quán)限數(shù)據(jù),但是不會觸發(fā)按鈕的重新渲染,那么我們就需要想辦法能讓它觸發(fā),這個可以使用watchEffect
方法,我們可以在updated
鉤子里通過這個方法將用戶權(quán)限數(shù)據(jù)和按鈕的更新方法關(guān)聯(lián)起來,這樣當用戶權(quán)限數(shù)據(jù)改變了,可以自動觸發(fā)按鈕的重新渲染:
js復(fù)制代碼import { createApp, reactive, watchEffect } from "vue" const codeList = reactive([1, 2, 3]) ?const hasPermission = (value) => { ?? ?return codeList.includes(value) } ?app.directive("auth", { ?? ?updated(el, binding) { ?? ? ? ?let update = () => { ?? ? ? ? ? ?let valueNotChange = binding.value === binding.oldValue ?? ? ? ? ? ?let oldHasPermission = hasPermission(binding.oldValue) ?? ? ? ? ? ?let newHasPermission = hasPermission(binding.value) ?? ? ? ? ? ?let permissionNotChange = oldHasPermission === newHasPermission ?? ? ? ? ? ?if (valueNotChange && permissionNotChange) return ?? ? ? ? ? ?if (newHasPermission) { ?? ? ? ? ? ? ? ?addEl(el) ?? ? ? ? ? ?} else { ?? ? ? ? ? ? ? ?removeEl(el) ?? ? ? ? ? ?} ?? ? ? ?}; ?? ? ? ?if (el._watchEffect) { ?? ? ? ? ? ?update() ?? ? ? ?} else { ?? ? ? ? ? ?el._watchEffect = watchEffect(() => { ?? ? ? ? ? ? ? ?update() ?? ? ? ? ? ?}) ?? ? ? ?} ?? ?}, })
將updated
鉤子里更新的邏輯提取成一個update
方法,然后第一次更新在watchEffect
中執(zhí)行,這樣用戶權(quán)限的響應(yīng)式數(shù)據(jù)就可以和update
方法關(guān)聯(lián)起來,后續(xù)用戶權(quán)限數(shù)據(jù)改變了,可以自動觸發(fā)update
方法的重新運行。