以前在公司一直使用的都是若依框架,所以对一些内容掌握的并不是很好,所以趁着在家的这段时间,自己写一个管理系统,那么就涉及到了动态路由,对不同权限的用户,展示不同菜单栏和向router中追加与该用户权限匹配的路由。
1.为什么使用动态路由
如果直接将路由表写死的话,那么在用户未登录的情况下,用户可以直接通过手动输入 url 达到目标页面。
当用户登录后,我们拿着后端返回的路由列表,去匹配本地动态路由列表中的路由 -> 路由对比
。
- 假设后端返回了该用户具有A路由,并且本地动态路由列表中也存在A路由,那么就直接将本地的A路由添加到router中。
- 当然也可以不选择路由对比,而是直接使用后端返回的路由列表。
- 但是如果不对比的话,后端返回的路由表中可能存在本地没有的路由文件,这样项目就会报错。
- 在后边的示例中,会加上路由对比这一步骤。
2.创建路由表
我们应该先写一份 公共的路由
,这个公共的路由可以在未登录的情况下访问,例如:Layout布局、登录页、404页。
- Layout布局肯定是不能在未登录的情况下访问,所以后续会使用全局前置路由守卫进行判断。
- 将它写在公共路由的原因是因为:后续追加的动态路由,需要添加到Layout布局路由的子节点路由中(嵌套路由)。
并且应该将这个公共路由表,添加到路由初始化时的 routes 属性中。
然后再将需要权限才能访问的路由写到一个数组中,这个数组就是本地的动态路由表,后续需要与后端返回的路由进行对比。
import { createRouter, Router, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import Layout from '@/layout/index.vue'; //Layout布局
// 公共路由表
const constantRoutes: RouteRecordRaw[] = [
{
path: '/',
name: 'Layout',
component: Layout,
},
{
path: '/login',
name: 'Login',
component: () => import('@/view/login.vue'),
meta: { title: '登录页' }
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/view/404.vue'),
meta: { title: '页面丢失了~' }
}
];
// 动态路由表
const asyncRoutes: RouteRecordRaw[] = [
{
path: '/',
name: '/',
component: () => import('@/view/index.vue'),
meta: { title: '主控台' }
},
{
path: '/goods/list',
name: '/goods/list',
component: () => import('@/view/commodity/commodity.vue'),
meta: { title: '商品管理' }
}
];
// 创建路由,并导出实例
export const router: Router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes
});
还差一个路由对比的功能,我们可以在上面的文件中,在写一个添加路由的函数,在里面实现路由对比,并且将它暴露出去。
// 参数:接收后端给的用户可访问的路由列表。
export function addRouters(menus: any[]){
// 功能:路由对比,将匹配的添加到路由
const routeComparison = (menus: any[])=>{
// 遍历后端返回的路由列表
menus.forEach((menuItem: any)=>{
// 判断当前遍历的这个路由,在本地动态路由列表是否存在,frontpath相当于本地的path
const isMatching = asyncRoutes.find((asyncItem: any)=>menuItem.frontpath === asyncItem.path);
// 如果匹配到了,则逻辑与一下:它是否已经被注册过了,如果没有被注册过我们在添加进去。
if(isMatching && !router.hasRoute(isMatching.path)){
// 将匹配出来的路由,添加到Layout布局路由的childrens中。
router.addRoute('Layout', isMatching);
}
// 判断当前遍历的这个路由是否有子节点,如果有子节点且子节点长度大于0,则进行递归,使用子节点进行对比。
if(mentItem.child && mentItem.child.length > 0){
routeComparison(mentItem.child);
}
});
}
// 调用路由对比
routeComparison(menus);
}
这个时候,我们可以完善一下全局前置路由守卫的代码,在src下新建:premission.ts文件,并在main.ts中引入。
import store from '@/store';
import { router, addRouters } from '@/router';
// 从Cookies中(获取|删除)Token。
import { getToken, removeToken } from '@/composables/auth';
// 是否获取了用户信息
const hasGetUserInfo = false;
// 全局路由前置守卫
router.beforeEach(async (to, from, next)=>{
// 获取Token
const token = getToken();
// 如果没有Token,并且访问的不是登录页,直接重定向到登录页,这里就会防止用户未登录直接进入Layout布局界面。
if(!token && to.path !== '/login'){
return next('/login');
}
// 如果有Token,但是访问的是登录页
if(token && to.path === '/login'){
// 从哪里来的,回哪里去
return next(from.path);
}
// 如果有Token,并且没有获取用户信息呢
if(token && !hasGetUserInfo){
// 拉取用户信息去,如果token过期或者被非法篡改,会在axios的拦截器中进行处理。
const getInfoRes = await store.dispatch("getInfo");
// 进行追加路由
addRouters(getInfoRes.menus);
// 将hasGetUserInfo置true
hasGetUserInfo = true;
}
// 在最后必须要放行
next();
});
在登录之后服务器会返回Token,然后将Token存储到cookies中,然后调用 router.push('/')
打算跳转到首页,但是会触发全局路由前置守卫,接着会同步获取用户信息,然后追加路由。
3.刷新后变404页或者空白页
这样动态路由就添加完了,但是存在着一个问题,下面进行问题复现:
- 当前浏览器的路由地址处于 asyncRoutes 列表上的某一个路由时,如果按F5刷新页面就会变成404或者空白。
- 如果 constRoutes 中存在匹配404的路由规则,那么就会显示404,否则就是空白页,并且控制台会有一个警告。
- 但是吧,你在追加完成路由以后,调用
router.getRoutes()
方法,又可以看到已经将路由追加进去了。
按照正常的代码逻辑来看,会执行以下三步:获取用户信息、追加路由、放行路由
。
- 这三个步骤都是同步执行的,并且我动态路由都已经添加好了,放行后应该不会出现问题啊。
4.问题分析:前奏:
首先我们将404这个路由项注释掉,并且在beforeEach()回调函数里的第一行打上断点,输入 debugger;
即可。
router.beforeEach((to, from, next)=>{
debugger;
//.......
});
在刷新时观察控制台,可以看到给我们抛出了一个警告,意思为:没有找到与路径对应的位置。
[Vue Router warn]: No match found for location with path "刷新的那个动态路由的path"
由此得知,在进入beforeEach()中的回调函数前,就已经出现问题了。
我们可以尝试将 debugger;
删除,在相同位置打印一下回调函数中的to属性。
console.log(to);
// 结果
{
"fullPath": "/goods/list",
"path": "/goods/list",
"query": {},
"hash": "",
"params": {},
"matched": [],
"meta": {},
"href": "#/goods/list"
}
可以看到matched
数组,是一个空的,代表着没有匹配到相关的路由。如果将 constRoutes 中的 404 规则注释删掉,那么这里的 matched
就会只有一个元素,就是404路由。
<router-view />
会渲染 matched
上的内容,我们的项目中共有两个 router-view
,在 App.vue 和 Layout 布局各有一个。
- 假设
matched
数组中有两个元素,第一个元素是 Layout 路由,第二个是 /goods/list 路由。 - 那么 App.vue 中的 router-view 就会渲染 Layout 布局组件,然后 Layout 布局中的 router-view 渲染 /goods/list 路由组件。
matched
元素越靠前,使用的 router-view 就越靠外层。
5.问题分析:原因:
其实这一切都与 to
有关,我们在刷新之后,触发了全局前置路由守卫,然后会调用它里面的回调函数。
那么在触发 beforeEach 的回调函数时,vueRouter 需要给 matched 设置匹配的路由,如果没有设置404页,那么这个数组它是空的,就会发出一个警告,既然是空的,那么没内容可以给 router-view 渲染,然后就会出现一个空白页。
如果有404页,在动态路由添加之前,输入一个不存在的地址,matched
数组的元素肯定是一个404,所以就会让router-view渲染404页呗~
总而言之,这个to,是动态路由没有追加进来时的to,所以才会这样。
6.解决问题:
首先我们要知道:在全局前置守卫中,next()、next('/')
两个的区别。
在全局前置守卫中,调用 next() 代表着放行的意思,而调用 next(‘/’) 代表着重定向的意思。
- 两个的区别在于:
next('/')会中断此导航,并重新触发路由守卫,而next()就不会,它就单纯的放行。
所以!!!!我们可以在追加路由后,重新触发一次路由守卫,而不是直接放行,这样就能解决问题了。
if(token && !hasGetUserInfo){
// 拉取用户信息去,如果token过期或者被非法篡改,会在axios的拦截器中进行处理。
const getInfoRes = await store.dispatch("getInfo");
// 进行追加路由
addRouters(getInfoRes.menus);
// 将hasGetUserInfo置true
hasGetUserInfo = true;
// 当添加完成后,直接进行一次重定向
return next(to.path);
}
// 一定要放行
next();
Ok!!问题解决~