vue权限控制和动态路由

一、拆分路由

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

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

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

任意路由:当路径出现错误的时候重定向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, constantRoutes } 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();
      //将比对后的路由组装到嵌套路由中
      constantRoutes[2].children.push(filterRoutes);
      // 通过router.addRoutes加入路由配置中
      constantRoutes.forEach((item) => {
        router.addRoute(item);
      });
      // 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。
          // 在路由守卫router.beforeEach中,使用addRoute,且next(to.fullPath)产生新的导航,否则仍然会404
          next(to.fullPath);
        } 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(",");
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容