vite+vue3+vue-router 根据接口权限,动态加载路由

307 阅读3分钟

1. 前因

公司的项目是一个toB项目,每个客户看到的页面都取决于权限配置情况。 用户不允许通过链接进入无权限页面。

2. 方案

  • 在全局路由钩子 beforeEach中,调用权限接口,获取用户权限列表,并判断用户是否有权限进入即将进入的页面。
  • 改写常用的路由方法:router.push router.resolve router.resolve,router.beforeResolve方法 调用方法后,检查将要进入的页面是否已经存在路由表中,如果存在则正常进入,否则通过router.addRoute将路由加入路由表后,再进入

3. 上代码

// router/hook.ts

    import type { RouteLocationNormalized } from "vue-router";
    import { storeToRefs } from "pinia";
    import { isUndef } from "@/lib/utils";
    import { ElMessage } from "element-plus";
    // 你自己的权限store文件
    import useXxxStore from "@/stores/xxx";

    // 检查用户是否有访问权限
    export async function checkAbility(to: RouteLocationNormalized, from: RouteLocationNormalized) {
        const xxxStore = useXxxStore()
        const xxxStoreRefs = storeToRefs(xxxStore)
        const rdata = xxxStore.getAbilities();
        if (rdata.status === 0) {
            const abilityKey = to.meta?.ability_key;
            if (abilityKey && isUndef(xxxStoreRefs.userAbilityMap.value[abilityKey])) {
                ElMessage.warning('您没有访问权限,可向管理员进行申请')
                return {
                    path: '/',
                    replace: true,
                }
            }
        } else {
            ElMessage.error(rdata1.message || rdata2.message || '获取系统配置失败')
        }
    }

// router/index.ts

    import { createRouter, createWebHistory, type RouteLocationRaw } from 'vue-router'
    // 不需要权限控制,所有用户都能访问的页面
    import aaa from './aaa';

    import { addRoute } from './routeList';
    
    import { checkAbility } from './hook'

    const router = createRouter({
      history: createWebHistory(import.meta.env.BASE_URL),
      routes: [
        ...aaa,
      ]
    })
    
    router.beforeResolve((to, from) => {
      const newRoute = addRoute(to)
      if (newRoute) {
        return {
          ...to,
          replace: true
        }
      }
    })

    const originalResolve = router.resolve;
    router.resolve = function resolve(to: RouteLocationRaw) {
        addRoute(to)
        return originalResolve.call(this, to)
    }

    const originalPush = router.push;
    router.push = function push(to: RouteLocationRaw) {
        addRoute(to)
        return originalPush.call(this, to)
    }
    
    const originalReplace = router.replace;
    router.replace = function replace(to: RouteLocationRaw) {
      addRoute(to)
      return originalReplace.call(this, to)
    }
    
    router.beforeEach(checkAbility)

    export default router

    

// router/routeList.ts

    import { isUndef } from '@/lib/utils';
    // 需要权限才能访问页面们
    import xxx from './xxx';
    
    import { storeToRefs } from "pinia";
    import router from '.';
    import { LocationAsRelativeRaw, MatcherLocationAsPath, RouteLocationRaw, RouteRecordRaw } from 'vue-router';
    
    // 实际数据应该取自权限接口
    const userAbilityMap = {}

    // 所有需要动态加入的路由列表
    export const ROUTE_LIST = [
        ...xxx,
    ]

    /**
     * 从路由表中查找路由
     * @param to
     */
    export function findRoute(to: RouteLocationRaw) {
        let temp: null | RouteRecordRaw = null;
        if (typeof to === 'string') {
            const path = to;
            temp = ROUTE_LIST.find(item => {
                const itemArr = item.path.split('/');
                // 计算路径中的变量
                const paramsLen = itemArr.filter(item => item.startsWith(':')).length;
                const pathStartIndex = path.indexOf(itemArr[0]);
                const pathArr = path.slice(pathStartIndex).split('/');
                return itemArr.slice(0, itemArr.length - paramsLen).every((item, index) => item === pathArr[index]);
            }) || null;
        } else if ((to as LocationAsRelativeRaw).name) {
            temp = ROUTE_LIST.find(item => item.name === (to as LocationAsRelativeRaw).name) || null;
        } else if ((to as MatcherLocationAsPath).path) {

            const path = (to as MatcherLocationAsPath).path;
            temp = ROUTE_LIST.find(item => {
                const itemArr = item.path.split('/');
                const paramsLen = itemArr.filter(item => item.startsWith(':')).length;
                const pathArr = path.split('/');
                return itemArr.slice(0, itemArr.length - paramsLen).every((item, index) => item === pathArr[index]);
            }) || null;
        }
        if (temp && router.hasRoute(temp.name as string)) {
            return;
        }
        
        const abilityKey = temp?.meta?.ability_key || null;
        return (abilityKey && !isUndef(userAbilityMap.value[abilityKey])) ? temp : null;
    }

    /**
     * 如果路由表中不存在需跳转路由,则添加,否则直接返回
     * @param to
     */
    export function addRoute(to:  RouteLocationRaw) {
        const newRoute = findRoute(to)
        newRoute && router.addRoute(newRoute)
        return newRoute
    }

// router/xxx.ts 路由示例

    import type { RouteRecordRaw } from 'vue-router';

    const routes: RouteRecordRaw[] = [
        {
            path: '/xxx/xxx',
            name: 'xxx',
            meta: {
                title: 'xxx',
                // 权限控制字段,根据实际情况设置
                ability_key: '',
            },
            component: () => import('../pages/xxx/xxxx.vue'),
        },
       
    ];

    export default routes;

4. 结论

功能实现:

  1. 根据后台权限配置,动态加载路由。

  2. 刷新页面,路由也能被正确加载,页面显示正常。

  3. 通过链接直接进入某个页面,页面通过权限检测后,可被正确加载和显示。

5. 备注

目前需要特殊处理的情况:

1. 不支持:在页面中使用 router.resolve,处理没有权限的页面,生成链接后,通过window.open 进行跳转。

    // 如果A用户没有 XXX页面的权限,window.open时会报错。
    // 推荐的解决方案是:先检测权限情况,如果没有权限不进行后续操作
    const route = router.resolve({
        name: 'XXX'
    })
    window.open(route.href, '_blank');

2. 使用beforeEnter钩子,进行路由中转

    const routes: RouteRecordRaw[] = [
        {
           ...
            beforeEnter(to, from) {
                // 如果希望在这里,中转到BBB页面,需要手动操作addRoute,然后再做跳转操作
                addRoute({
                    name: 'BBB',
                })
                return {
                    replace: true,
                    name: 'BBB',

                }
            }
        },
       
    ];

微信图片_20240117184635.jpg