Vue3 权限管理从零开始:新手指南

前言

这篇文章是写给刚入门 Vue 框架开发或者刚入门的同学,权限管理是基本上每一个项目都会遇到问题,你有登陆,有游客等身份,你总要不同的身份会有不同的网页访问吧。下面就实现从想法到权限管理的构建。

必备条件

  1. Vue3 项目开发语言基础了解

  2. Vue-router 路由管理基础了解

  3. Pina 状态管理器基础了解

  4. 首先执行创建工程语句

    1
    pnpm create vite my-vue-app --template vue-ts

    这样就得到了一个 vue3+vite 项目。

  5. 安装路由、状态管理器以及持久化插件

    1
    pnpm install vue-router@4 pinia pinia-plugin-persistedstate

    这个目的是存储我们的用户信息和当前的路由表

大致想法

graph TD
    A[用户访问主页] --> B{是否已登录?}
    B -- 否 --> C[仅可以查看公告页面]
    B -- 是 --> D[记录身份信息]
    D --> E[记录用户能访问的路由表]
    E --> F{用户访问页面}
    F -- 属于路由表 --> G[加载页面]
    F -- 不属于路由表 --> H[跳转到 /error 页面]

这样的话就完成了用户的基础路由权限设计。

操作演示

实际的路由表的更新主要是通过 addRouteremoveRoute 来控制。

  1. src/pages 下面新建 admin.vuecustom.vueerror.vuehome.vueuser.vue 。分别写入页面名称就行。例如 amdin.vue 页面内容。

    1
    2
    3
    4
    5
    6
    <template>
    <div>我是admin页面</div>
    </template>

    <script setup lang="ts"></script>
    <style scoped lang="scss"></style>
  2. src/store 中新建 permission.ts 写入下面的内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    import { defineStore } from "pinia";
    import { publicRoutes, privateRoutes } from "../router";
    import { RouteRecordRaw } from "vue-router";

    export const useRoutesStore = defineStore("routes", {
    state: () => ({
    routes: publicRoutes,
    }),

    actions: {
    setRoutes(newRoutes: RouteRecordRaw[]) {
    this.routes = [...publicRoutes, ...newRoutes];
    },
    filterRoutes(role: string) {
    this.routes = [
    ...publicRoutes,
    ...privateRoutes.filter((item) => item.meta.role === role),
    ];
    return this.routes;
    },
    },
    persist: {
    storage: localStorage,
    },
    });

    src/store 中新建 user.ts 写入下面的内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import { defineStore } from "pinia";

    export type Role = "admin" | "custom" | "user";

    export const useUserRoleStore = defineStore("userInfo", {
    state: (): { role: Role } => ({
    role: "user",
    }),
    getters: {
    getRole: (state) => state.role,
    },
    actions: {
    changeRole(role: Role) {
    this.role = role;
    },
    },
    persist: {
    storage: window.localStorage,
    },
    });
  3. 可以对路由文件进行划分,这样会显得目录清晰。例如我建立了 admincustomuser。三个私有路由的文件,然后导入了一个文件中再合并成一个私有路由参数。当然你可以分的更细致,我这里只做个 demo
    例如 src/router/admin.ts 内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const Admin = () => import("../pages/admin.vue");
    export const adminRoutes = [
    {
    name: "admin",
    component: Admin,
    children: [],
    path: "/admin",
    meta: { role: "admin" },
    },
    ];
  4. 在路由总文件处注册路由src/router/index.ts
    这个文件大致要做 4 件事,当然你也可以细致的分划一下。

    1. 导入私有路由的组合和公共路由
    2. 使用createRouter 创建路由
    3. 编写全局路由功能:跳转前,看是路由是否存在
    4. 抛出修改路由表的方法

    那么他的具体内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    import {
    createWebHashHistory,
    createRouter,
    RouteRecordRaw,
    } from "vue-router";
    import { useUserRoleStore } from "../store/user";

    import { userRoutes } from "./user";
    import { adminRoutes } from "./admin";
    import { customRoutes } from "./custom";
    import { useRoutesStore } from "../store/permission";

    // 导出所有的私密路由
    export const privateRoutes = userRoutes
    .concat(adminRoutes)
    .concat(customRoutes);

    const Home = () => import("../pages/home.vue");
    const Error = () => import("../pages/error.vue");

    export const publicRoutes: RouteRecordRaw[] = [
    { name: "home", component: Home, children: [], path: "/" },
    { name: "error", component: Error, children: [], path: "/error" },
    ];

    export const router = createRouter({
    history: createWebHashHistory(),
    routes: publicRoutes,
    });

    //TODO 全局路由,跳转前,看是路由是否存在
    // 假设用户时已经登陆的,这里只是做路由的动态修改操作
    router.beforeEach(async (to, from, next) => {
    const userRoleStore = useUserRoleStore();
    const routeStore = useRoutesStore();
    const filterRoutes = routeStore.filterRoutes(userRoleStore.role);
    filterRoutes.forEach((item) => {
    router.addRoute(item);
    });
    const exist = router.getRoutes().some((route) => route.path === to.path);
    console.log("跳转前", router.getRoutes(), to.path, exist);
    if (exist) {
    next();
    } else {
    next(false);
    router.push("/error");
    }
    });

    //TODO 抛出添加路由的方法

    export const filterRoleRoutes = async () => {
    const userRoleStore = useUserRoleStore();

    let preRoutes = router.getRoutes();
    preRoutes.forEach((route) => {
    if (
    route.meta.role &&
    route.meta.role !== userRoleStore.role &&
    route.name
    ) {
    router.removeRoute(route.name.toString());
    }
    });
    };

    如果仅仅只是上面的操作的话,那么会出现用户切换了身份,路由更新了,但是点击对应的页面,他没法跳转。以及刷新的时候,路由表还没更新到当前用户的路由表,会无法访问具体的私有路由页面。

  5. 这里就是通过 App.vue 里的逻辑来改造上面描述的问题。当然里面的函数和方法你也可以封装和优化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    <template>
    <div>
    <span>切换身份:</span>
    <button @click="changeRoleFn('user')">user</button>
    <button @click="changeRoleFn('custom')">custom</button>
    <button @click="changeRoleFn('admin')">admin</button>
    </div>
    <div>
    <span>跳转路由</span>
    <button
    @click="
    () => {
    router.push('/user');
    }
    ">
    user
    </button>
    <button
    @click="
    () => {
    router.push('/custom');
    }
    ">
    custom
    </button>
    <button
    @click="
    () => {
    router.push('/admin');
    }
    ">
    admin
    </button>
    </div>
    <div>
    <RouterView></RouterView>
    </div>
    </template>

    <script setup lang="ts">
    import { filterRoleRoutes } from "./router";
    import { Role, useUserRoleStore } from "./store/user";
    import { useRouter } from "vue-router";
    import { useRoutesStore } from "./store/permission";

    const userRoleStore = useUserRoleStore();
    const router = useRouter();
    const routeStore = useRoutesStore();

    const changeRoleFn = async (role: Role) => {
    userRoleStore.changeRole(role);

    // 内存添加路由表
    const filterRoutes = routeStore.filterRoutes(userRoleStore.role);
    filterRoutes.forEach((item) => {
    router.addRoute(item);
    });
    // 路由更新,删除不符合当前身份的路由
    filterRoleRoutes();
    routeStore.filterRoutes(role);
    };

    // TODO每次刷新的时候检查下更新路由表
    const filterRoutes = routeStore.filterRoutes(userRoleStore.role);
    filterRoutes.forEach((item) => {
    router.addRoute(item);
    });

    // TODO 这里如果不手动更新跳转的话,route.path的值实际上是'/' 而不是你浏览器上显示的网络地址
    router.push(location.hash.replace(/#/, ""));
    </script>

    <style scoped></style>

    代码上有详细的解释。其实原理很简单,主要是踩的坑会比较麻烦。

功能权限控制

上面一块是大的页面权限的控制,那么同一个身份下的用户,有不同的功能。对于功能也是需要区分开的。

想法

通过指令来决定当前的这个 dom 是不是要渲染,如果身份不对就不渲染。不渲染你想点击也没法点。

那么下面就简单的给出部分代码

新建一个指令文件夹,编写permission指令代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { useUserRoleStore } from "../store/user";

function checkPermission(el: any, binding: any) {
const userRoleStore = useUserRoleStore();
// 获取绑定的值,此处为权限
const { value } = binding;
const userName = userRoleStore.name;

// 当传入的指令集为数组时
if (value && value instanceof Array) {
// 匹配对应的指令
const hasPermission = value.includes(userName);
// 如果无法匹配,则表示当前用户无该指令,那么删除对应的功能按钮
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el);
}
} else {
// eslint-disabled-next-line
throw new Error('v-permission value is ["admin","editor"]');
}
}

export default {
// 在绑定元素的父组件被挂载后调用
mounted(el: any, binding: any) {
checkPermission(el, binding);
},
// 在包含组件的 VNode 及其子组件的 VNode 更新后调用
update(el: any, binding: any) {
checkPermission(el, binding);
},
};

后面就是注册到 app 上。当然了最好是弄一个单独的文件,然后统一的注册。单独注册的语法如下app.directive("permission", permission); 我这里就省略了,具体可以看源码

那么使用的话就只需像如下的admin.vue页面一样

1
2
3
4
5
6
7
8
9
<template>
<div>我是admin页面</div>
<!-- 因为我在useStore里面写死了name为david,所以效果就是只有david这个按钮显示 -->
<button v-permission="['david']">david可以点</button>
<button v-permission="['lisa']">lisa可以点</button>
</template>

<script setup lang="ts"></script>
<style scoped lang="scss"></style>

源码以及效果

效果就是,你切换对应的身份,那么他身份下的路由页面,你可以访问,相反的不是他的路由页面,你只能访问到/error 页面。

当然你可以不用动态路由的方法来实现,你也可以仅仅通过全局理由beforeEach 来判断当前要去的页面是否能被访问

https://github.com/HideInMatrix/vue3-router-permission