美文网首页vue
vue权限控制和动态路由

vue权限控制和动态路由

作者: 扶得一人醉如苏沐晨 | 来源:发表于2023-10-27 16:11 被阅读0次

    一、拆分路由

    将路由拆分为三部分:常量路由、需要异步加载的路由、任意路由

    常量路由:就是不关用户是什么角色,都可以看见的路由

    异步路由:未来会根据角色被过滤的路由
    一般这种情况是所有菜单已知的情况下,定义出所有菜单的全量路由,后面根据后端返回的权限菜单,来过滤这个全量路由,达到不同角色展示不同的菜单权限的效果

    任意路由:当路径出现错误的时候重定向404

    src/router/index.js写入如下代码

    //引入Vue|Vue-router
    import Vue from "vue";
    import Router from "vue-router";
    
    //使用路由插件
    Vue.use(Router);
    
    /* 引入最外层骨架的一级路由组件*/
    import Layout from "@/layout";
    
    //常量路由:就是不关是什么角色,都可以看见的路由
    //什么角色(超级管理员,普通员工):登录、404、首页
    export const constantRoutes = [
      {
        path: "/login",
        component: () => import("@/views/login/index"),
        hidden: true,
      },
    
      {
        path: "/404",
        component: () => import("@/views/404"),
        hidden: true,
      },
    
      {
        path: "/",
        component: Layout,
        redirect: "/dashboard",
        children: [
          {
            path: "dashboard",
            name: "Dashboard",
            component: () => import("@/views/dashboard/index"),
            meta: { title: "首页", icon: "dashboard" },
          },
        ],
      },
    ];
    
    //异步理由:不同的(角色),需要过滤筛选出的路由,称之为异步路由
    //有的角色可以看见产品管理、有的可以看见订单管理
    export const asyncRoutes = [
     {
        path: "/product", //产品管理
        name: "product",
        component: () => import("@/views/product/Index.vue"),
        redirect: "/product/list",
        meta: {
          title: "产品管理",
        },
        children: [
          {
            path: "list", //访问路径: /product/list
            name: "list",
            component: () => import("@/views/product/list/Index.vue"),
            meta: {
              title: "产品列表",
            },
          },
          {
            path: "category",
            name: "category",
            component: () => import("@/views/product/category/Index.vue"),
            meta: {
              title: "产品分类",
            },
          },
        ],
      },
      {
        path: "/order", //订单管理
        name: "order",
        component: () => import("@/views/order/Index.vue"),
        redirect: "/order/order-list",
        meta: {
          title: "订单管理",
        },
        children: [
          {
            path: "order-list",
            name: "order-list",
            component: () => import("@/views/order/list/Index.vue"),
            meta: {
              title: "订单列表",
            },
          },
          {
            path: "contract",
            name: "contract",
            component: () => import("@/views/order/contract/Index.vue"),
            meta: {
              title: "订单审核",
            },
          },
        ],
      },
    ];
    
    //任意路由:当路径出现错误的时候重定向404
    export const anyRoutes = { path: "*", redirect: "/404", hidden: true };
    
    const createRouter = () =>
      new Router({
        // mode: 'history', // require service support
        scrollBehavior: () => ({ y: 0 }),
        //因为注册的路由是‘死的’,‘活的’路由如果根据不同用户(角色)可以展示不同菜单
        routes: [...constantRoutes, ...anyRoutes],
      });
    
    const router = createRouter();
    
    // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
    export function resetRouter() {
      const newRouter = createRouter();
      // router.matcher是比较核心的一个属性。对外提供两个方法match(负责route匹配), addRoutes(动态添加路由)。
      // 对router.matcher属性做修改,即新的routes就会替换老的routes, 其实就是replaceRoutes()的含义(但是官方没有提供这个API)。
      router.matcher = newRouter.matcher; // reset router
    }
    
    export default router;
    

    router.matcher是比较核心的一个属性

    router.matcher属性做修改,即新的routes就会替换老的routes

    二、根据登录用户获取用户信息,并且动态添加路由

    思路

    • 用户登录成功,将token存储进入vuex
    • 调用getUserInfo接口,获取用动态路由dyRoutes
    • 根据dyRoutes过滤我们定义的asyncRoutes得到过滤后的动态filterRoutes
    • filterRoutes存到vuex
    • 调用resetRouter()方法,将路由恢复到初始状态
    • 将比对后的路由通过router.addRoutes(filterRoutes);加入路由配置中

    2.1、封装vuex的user模块 src/store/modules/user.js

    代码如下

    /* 引入前端定义的路由,和重置路由的方法 */
    import { asyncRoutes, resetRouter } from "@/router";
    /* 引入路由对象 */
    import router from "@/router";
    /* 深拷贝方法 */
    import cloneDeep from "lodash/cloneDeep";
    /* 引入登录api */
    import { loginByUsername } from "@/api/login";
    /* 获取用户信息的接口 */
    import { getUserInfo } from "@/api/user";
    
    //asyncRoutes前端定义的路由  dyRoutes后端返回的路由
    const computedAsyncRoutes = (asyncRoutes, dyRoutes) => {
      //定义存储匹配好的路由容器
      let filterRoutes = [];
      // 深拷贝前端路由
      let asyncRoutesTmp = cloneDeep(asyncRoutes);
      asyncRoutesTmp.forEach((one) => {
        dyRoutes.forEach((two) => {
          if (one.name === two.name) {
            //继续判断下级菜单 children
            if (two.children && two.children.length > 0) {
              one.children = computedAsyncRoutes(one.children, two.children);
            }
            filterRoutes.push(one);
          }
        });
      });
      return filterRoutes;
    };
    const user = {
      state: {
        /* 用户的按钮权限 */
        permissions: [],
        /* 用户角色 */
        roles: [],
        /* 根据asyncRoutes和服务端返回的路由过滤出来的路由 */
        filterRoutes: [],
        /* token */
        token: "",
      },
      actions: {
        // 根据用户名登录
        LoginByUsername({ commit }, userInfo) {
          return new Promise((resolve, reject) => {
            loginByUsername(userInfo.username, userInfo.password)
              .then((response) => {
                /* 存储token */
                commit("SET_TOKEN", response.data.token);
                resolve();
              })
              .catch((error) => {
                reject(error);
              });
          });
        },
        // 查询用户信息
        GetUserInfo({ commit }) {
          return new Promise((resolve, reject) => {
            getUserInfo()
              .then((res) => {
                const data = res.data.data || {};
                /* 存储用户角色 */
                commit("SET_ROLES", data.roles || []);
                /* 存储用户按钮权限 */
                commit("SET_PERMISSIONS", data.permissions || []);
                /* 存储比对后的动态路由 */
                commit(
                  "SET_FILTER_ROUTES",
                  computedAsyncRoutes(asyncRoutes, data.routes)
                );
                resolve(data);
              })
              .catch(() => {
                reject();
              });
          });
        },
      },
      mutations: {
        /* 存储token */
        SET_TOKEN: (state, token) => {
          state.token = token;
        },
        /* 存储按钮权限 */
        SET_PERMISSIONS: (state, permissions) => {
          state.permissions = permissions;
        },
        /* 存储用户角色信息 */
        SET_ROLES: (state, roles) => {
          state.roles = roles;
        },
        //存储最终比对后的路由信息
        SET_FILTER_ROUTES: (state, filterRoutes) => {
          state.filterRoutes = filterRoutes;
          //添加路由之前 清空路由实例内容
          resetRouter();
          //将比对后的路由通过router.addRoutes加入路由配置中
          router.addRoutes(filterRoutes);
        },
      },
    };
    export default user;
    

    2.2、引入user模块 src/store/index.js

    import Vue from "vue";
    import Vuex from "vuex";
    import user from "./modules/user";
    import getters from "./getters";
    
    Vue.use(Vuex);
    const store = new Vuex.Store({
      modules: {
        user,
      },
      getters,
    });
    
    export default store;
    

    三、设置全局前置守卫 router.beforeEach,当用户信息、路由信息丢失时重新获取,解决页面刷新时数据丢失问题

    3.1、src/permission.js

    const whiteList = ["/login"];
    /* 引入路由对象 */
    import router from "@/router";
    /* 引入store对象 */
    import store from "@/store";
    router.beforeEach(async (to, from, next) => {
      /* 如果有token */
      if (store.state.user.token) {
        /* 用户去登录页--直接跳转到layout首页 */
        if (to.path === "/login") {
          next({ path: "/" });
        } else {
          //判断当前存储的vuex里面是否已经有动态路由了
          if (store.state.user.filterRoutes.length != 0) {
            //有动态路由,放行
            next();
          } else {
            //没有路由
            try {
              /* 获取动态路由--并添加通过addRoutes到router实例 */
              await store.dispatch("GetUserInfo");
              // hack方法 确保addRoutes已完成
              // 其实在路由守卫中,只有next()是放行
              // 其他的诸如:next('/logon') 、 next(to) 或者 next({ ...to, replace: true })都不是放行,
              // 而是:中断当前导航,执行新的导航,会重新进入router.beforeEach。
              // next({ ...to, replace: true })中的replace: true只是一个设置信息,告诉VUE本次操作后,不能通过浏览器后退按钮,返回前一个路由。
              next({ ...to, replace: true });
            } catch (error) {
              next(`/login?redirect=${to.path}`);
            }
          }
        }
      } else {
        if (whiteList.indexOf(to.path) !== -1) {
          next();
        } else {
          next(`/login?redirect=${to.path}`);
        }
      }
    });
    

    四、总结

    1、router.addRoutes()函数的作用:给路由器添加新的路由,并且是在已有的路由上再添加路由,并不会覆盖原有路由,所以我们在重复添加路由的时候,可能就会出现警告 [vue-router] Duplicate named routes definition: { name: "Dashboard", path: "/dashboard" },意思是你重复添加了 nameDashboard的路由。

    2、为什么 router.addRoutes() 添加新路由后,需要手动修改router.options.routes
    router.options是我们创建路由实例,传入的配置项,router.addRoutes动态添加路由后并不会修改router.options.routes,Vue这么设计的,所以需要手动修改 router.options.routes。

    3、页面刷新后,vuex数据丢失,同样的动态添加的路由也会丢失,所以需要重新添加路由信息,并且刷新后直接访问动态添加的路由会出现白屏问题,因为动态路由是异步加载的,我们就直接访问了还没有加载的页面,所以会出现白屏问题,我们需要中断当前导航,执行新的导航,即重新访问一次路由才行。所以需要用到全局前置守卫 router.beforeEach重新获取数据,并且需要用到 next({...to, replace:true}) 重新访问一次路由才行。

    4、next()是放行,其他的诸如:next('/logon')next(to) 或者 next({ ...to, replace: true })都不是放行,而是:中断当前导航,执行新的导航,会重新进入 router.beforeEach

    5、next({ ...to, replace: true })中的replace: true只是一个设置信息,告诉VUE本次操作后,不能通过浏览器后退按钮,返回前一个路由。

    6、一定要确保 addRoutes() 已经完成时,再执行下一次beforeEach((to, from, next)

    7、如果守卫中没有正确的放行出口的话,会一直next({ ...to})进入死循环 !!!

    8、当我们分配权限的时候、一定要注意子路由和父路由的关系,因为我们可能只勾选了子路由却没有勾选父路由,这种情况下是无法访问子路由的,因为父路由不存在。实际我们就应该直接将勾选的子路由上的父路由也保存下来,否则父路由没有的情况下,我们是无法访问子路由的,也添加不上。所以再好的办法是,直接将勾选的子路由上的父路由也传递给后台,保存下来。

      // 解决elementUI树形控件子节点勾选,但是半勾选父节点未被保存的的问题
    const ids = Array.from(
        new Set([
          ...this.$refs.tree.getHalfCheckedKeys(),
          ...this.$refs.tree.getCheckedKeys(),
        ])
      ).join(",");
    

    相关文章

      网友评论

        本文标题:vue权限控制和动态路由

        本文链接:https://www.haomeiwen.com/subject/exguidtx.html