一、拆分路由
将路由拆分为三部分:常量路由、需要异步加载的路由、任意路由
常量路由:就是不关用户是什么角色,都可以看见的路由
异步路由:未来会根据角色被过滤的路由
一般这种情况是所有菜单已知的情况下,定义出所有菜单的全量路由,后面根据后端返回的权限菜单,来过滤这个全量路由,达到不同角色展示不同的菜单权限的效果
任意路由:当路径出现错误的时候重定向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" }
,意思是你重复添加了name
为Dashboard
的路由。
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(",");
网友评论