以前在公司一直使用的都是若依框架,所以对一些内容掌握的并不是很好,所以趁着在家的这段时间,自己写一个管理系统,那么就涉及到了动态路由,对不同权限的用户,展示不同菜单栏和向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!!问题解决~