vue3权限管理——(路由权限)动态路由设置

89 篇文章 7 订阅

1.大概思路

  1. 设置基础路由login和home等页面;
  2. 登录后从后端获取user,token,rights等数据,并将数据同时存储到vuex和sessionStorage中
  3. 将后端获取的权限数据(作为不同用户显示不同菜单及不同路由的依据)和路由页面进行映射;
  4. 写公共方法添加动态路由;
  5. 在路由全局守卫中判断是否已添加过动态路由,如果已添加直接next()未添加调用添加动态路由方法,并且放行(next({ ...to, replace: true }););
  6. 添加动态路由位置第二种方法:全局路由中只负责判断是否有动态路由和放行,调用添加动态路由方法,在登录后立即调用,且以防页面手动刷新时进行404页面,需要在App.vue的create()方法中再次调用

2.设置基础路由,login页面,Home页面等

//route.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
    { name: 'Login', path: '/login', component: Login },
    {
        name: 'Home',
        path: '/',
        component: Home,
        // 到home页面后直接跳转到excelPreview页面
        redirect: '/uploadSpec?active=0',
        meta: {
            authentication: true
        },
        // 整个页面为Home,如果想要点击el-aside左侧菜单,将对应路由内容显示在el-main区域,需要将其他路由路径设置为Home的子路径,且path前没有/
        children: [
            {
                name: 'UploadSpec',
                path: 'uploadSpec',
                title: '上传spec',
                component: UploadSpec,
                // 组件内传参
                meta: {
                    keepAlive: true,
                    authentication: true,
                    // 用于设置tab名
                    title: '上传spec',
                }
            },
        ]
    },
    // vue3不再使用path:'*'正则匹配,而是使用/pathMatch(.*)*或/pathMatch(.*)或/catchAll(.*)
    // { name: 'NotFound', path: '/404', component: NotFound },
    // { path: '/:pathMatch(.*)', redirect: '/404' },
];

const router = createRouter({
    history: createWebHistory(),
    routes,
});

2.从后端获取菜单权限数据并设置到vuex和sessionStorage中

这里使用mock模拟后端返回数据

//mock/user.js
import Mock from 'mockjs'

const users = [
    {'id': 1, path:'/uploadSpec','authName': "上传spec", 'icon': '',children:[]},
    {'id': 2, path:'/showSpec', 'authName': "Spec预览", 'icon': '',children:[]},
    {'id': 3, path:'/generateTxt', 'authName': "生成测试数据", 'icon': '',children:[]},
    {'id': 4, path:'/generateCronjob', 'authName': "生成转码程序", 'icon': '',children:[]},
    {'id': 5, path:'/pdfCompare', 'authName': "PDF文档对比", 'icon': '',children:[]},
    {'id': 6, path:'/resourceUpdate', 'authName': "资源更新管理", 'icon': '',children:[]},
    {'id': 7, path:'/generateTestCase', 'authName': "自动生成ST/SIT案例", 'icon': '',children:[]},
    {'id': 8, path:'/userManagement', 'authName': "用户管理", 'icon': 'User'},
]

Mock.mock("/user/login", Mock.mock({
    "code": 200,
    "success": true,
    "data": {
        users: users,
        "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlck5hbWUiOiJhZG1pbiIsIm5pY2tOYW1lIjoi6LaF566hIiwiaWNvbiI6IiIsInJvbGVJZCI6MSwic3ViIjoiYWRtaW4iLCJleHAiOjE2OTI3NzMzNTMsImp0aSI6ImZkNmVkOWZiMjdiYzQxODg5OWRmYmYzNzhlMTMzZmQ0In0.APGpN-i2edKwPQA52LP10aDEM2DZi7G71k8f_njGcpE"
    }
})
)

Mock.mock("/user/me", Mock.mock({
    "code": 200,
    "success": true,
    "data": {
        "id": 1,
        "userName": "admin",
        "nickName": "超管",
        "icon": "",
        "roleId": 1,
        "rights": users,
    }
})
)

3.登录后将数据存到vuex和sessionStorage中

//Login.vue           
 await this.$store.dispatch("user/login", {
              ...this.user
            });

//store/user/user.js
import * as api from '@/api/api'
import { ElMessage } from 'element-plus'

const state = () => {
    return {
        user:JSON.parse(sessionStorage.getItem("user") || '{}'),
        rights: JSON.parse(sessionStorage.getItem("rights") || '[]')
    }
}

const getters = () => {}

const actions = {
    // 注意async位置和箭头函数写法
    login: async({ commit }, user) => {

        // 调用登录接口
        try {
            let result = await api.login(user);
            if (result.data.code === 200) {
                sessionStorage.setItem("token", result.data.data.token);
                // 调用获取user信息接口
                let loginUser = await api.getLoginUser();
                
                // 调用mutations的login方法
                if (loginUser.data.code === 200) {
                    commit('login', loginUser.data.data);
                } else {
                    ElMessage.error("登录失败:用户信息获取失败");
                }
            } else {
                ElMessage.error("登录失败:" + result.data.errorMsg);
            }
        } catch (error) {
            throw error;
        }
    },
}

const mutations = {
    initUser: (state) => {
        // 从localStorage中获取数据设置进user中,否则通过刷新页面时,获取不到state中的user信息
        state.user = JSON.parse(sessionStorage.getItem("loginUser"));
    },

    login: (state, user) => {
        // 登录成功后将user信息存到state中,且缓存到localStorage中
        state.user = user;
        state.rights = user.rights;
        sessionStorage.setItem("loginUser", JSON.stringify(user));
        sessionStorage.setItem("rights", JSON.stringify(user.rights));
    },
}

export default {
    namespaced: true,
    state,
    getters,
    actions,
    mutations
}

4.设置菜单权限数据和路由的映射

注意此处routeMapping的key和uploadSpecRule的path以及和后端返回数据rights的user.js的path必须一致;且路径必须有 "/"

const uploadSpecRule = {
    name: 'UploadSpec',
    path: '/uploadSpec',
    title: '上传spec',
    component: UploadSpec,
    meta: {
        keepAlive: true,
        authentication: true,
        title: '上传spec',
    }
};

const showSpecRule = {
    name: 'ExcelPreview',
    path: '/showSpec',
    title: 'Spec预览 | 生成测试数据',
    component: ExcelPreview,
    meta: {
        keepAlive: true,
        authentication: true,
        title: 'Spec预览',
    }
};

const generateTxtRule = {
    name: 'GenerateTxt',
    path: '/generateTxt',
    title: '生成测试数据',
    component: GenerateTxt,
    meta: {
        keepAlive: true,
        authentication: true,
        title: '生成测试数据',
    }
};
const generateCronjobRule = {
    name: 'GenerateCronjob',
    path: '/generateCronjob',
    title: '生成转码程序',
    component: GenerateCronjob,
    meta: {
        keepAlive: true,
        authentication: true,
        title: '生成转码程序',
    }
};
const pdfCompareRule = {
    name: 'PDFCompare',
    path: '/pdfCompare',
    title: 'PDF文档对比',
    component: PDFCompare,
    meta: {
        keepAlive: true,
        authentication: true,
        title: 'PDF文档对比',
    }
};
const resourceUpdateRule = {
    name: 'ResourceUpdate',
    path: '/resourceUpdate',
    title: '资源更新管理',
    component: ResourceUpdate,
    meta: {
        keepAlive: true,
        authentication: true,
        title: '资源更新管理',
    }
};
const generateTestCaseRule = {
    name: 'GenerateTestCase',
    path: '/generateTestCase',
    title: '自动生成ST/SIT案例',
    component: GenerateTestCase,
    meta: {
        keepAlive: true,
        authentication: true,
        title: '自动生成ST/SIT案例',
    }
};
const userManagementRule = {
    name: 'UserManagement',
    path: '/userManagement',
    title: '用户管理',
    component: UserManagement,
    meta: {
        keepAlive: true,
        authentication: true,
        title: '用户管理',
    }
};

const routeMapping = {
    '/uploadSpec': uploadSpecRule,
    '/showSpec': showSpecRule,
    '/generateTxt': generateTxtRule,
    '/generateCronjob': generateCronjobRule,
    '/pdfCompare': pdfCompareRule,
    '/resourceUpdate': resourceUpdateRule,
    '/generateTestCase': generateTestCaseRule,
    '/userManagement': userManagementRule,
}

5.设置可导出添加动态路由公共方法

export const initDynamicRoutes = async () => {
    const rightsList = Store.state.user.rights;;

    rightsList.length > 0 && rightsList.forEach(item => {
        if (item.path) {
            const temp = routeMapping[item.path];
            router.addRoute("Home", temp);
        }
    });
    router.addRoute( { name: 'NotFound', path: '/404', title:"页面不存在",component: NotFound });
    router.addRoute( { path: '/:pathMatch(.*)', redirect: '/404' });
}

5.全局路由守卫中鉴权及激活动态路由

注意在vue-router4.X中,动态路由主要通过两个函数实现。router.addRoute() 和 router.removeRoute()。它们注册一个新的路由,也就是说,如果新增加的路由与当前位置相匹配,就需要你用 router.push() 或 router.replace() 来手动导航,才能显示该新路由。

也就是说以前的rouer.addRoutes()添加即可使用动态路由的方式不行了

// 路由守卫鉴权处理
router.beforeEach(async (to, from, next) => {
    let token = sessionStorage.getItem("token");
    let isToken = !!token;
    let loginUser = Store.state.user.user;

    if (isToken) {
        if (loginUser) {
            // 如果是login,且已经登录了直接跳转到home页面
            if (to.name === "Login") return next();

            // 已经设置过动态路由直接放行,没有则需要通过手动调用 router.replace()进行路由显示
            if (isDynamic) {
                next();
            } else {
                await initDynamicRoutes();
                isDynamic = true;
                next({ ...to, replace: true });
            }
        } else {
            sessionStorage.clear();
            next("/login");
        }
    } else {
        isDynamic = false;
        next();
    }
});

6.第二种方法激活动态路由——在Login.vue和App.vue中调用

 这种方式不会每次路由都调用一次添加动态路由方法,个人觉得这种方式性能比较好

//App.vue
import { initDynamicRoutes } from "@/route/route"
export default {
  name: 'App',
  created(){
    initDynamicRoutes();
  }
}
</scri
//Login.vue
          try {
            await this.$store.dispatch("user/login", {
              ...this.user
            });

            //登录成功后,根据用户的rights动态添加路由
            initDynamicRoutes();

            this.$router.push({ name: "Home" });
          } catch (e) {}

7.注意点:

7.1 必须使用router.addRoute()方法进行添加动态路由

添加动态路由方法中,是使用router.addRoute()方法进行添加,Home表示将路由嵌套在Home页面下

rightsList.length > 0 && rightsList.forEach(item => {
        if (item.path) {
            const temp = routeMapping[item.path];
            router.addRoute("Home", temp);
        }
    });

我试过之前的添加方法已经不能成功添加动态路由了

const currentRoutes = router.options.routes;

    rightsList.length > 0 && rightsList.forEach(item => {
        if (item.path) {
            const temp = routeMapping[item.path];
            currentRoutes[1].children.push(temp);
        }
    });

7.2 错误页面添加时机

错误页面必须也使用动态添加方法,并且在路由动态添加完成后再添加,否则如果在基础路由中定义错误页面,那么每次点击路由都会首先找路由,没找到就会匹配到错误页面,从而跳转到错误页面

rightsList.length > 0 && rightsList.forEach(item => {
        if (item.path) {
            const temp = routeMapping[item.path];
            router.addRoute("Home", temp);
            // currentRoutes[1].children.push(temp);
        }
    });
    router.addRoute( { name: 'NotFound', path: '/404', title:"页面不存在",component: NotFound });
    router.addRoute( { path: '/:pathMatch(.*)', redirect: '/404' });

7.3判断动态路由是否启用的标识

如果直接next()会找不到动态路由,而直接next({ ...to, replace: true });会导致永远去找动态路由

使用标识后,如果有动态路由就加载动态路由,没有直接放行

if (isToken) {
        if (loginUser) {
            // 如果是login,且已经登录了直接跳转到home页面
            if (to.name === "Login") return next();

            // 已经设置过动态路由直接放行,没有则需要通过手动调用 router.replace()进行路由显示
            if (isDynamic) {
                next();
            } else {
                // await initDynamicRoutes();
                isDynamic = true;
                next({ ...to, replace: true });
            }
        } else {
            sessionStorage.clear();
            next("/login");
        }
    } else {
        isDynamic = false;
        next();
    }

7.4警告:[Vue Router warn]: No match found for location with path "/generateCronjob"

以7.2的方式,在动态添加完其他路由后,再添加错误页面就能成功导到不同路由页面,但是警告依然会存在

8.重点重点20231204补充 :警告:[Vue Router warn]: No match found for location with path "/generateCronjob"

针对这个警告,报错的意思就是打开地址/generateCronjob时没有找到。其实菜单数据是登录后就放到vuex和localStorage里面的,并且针对这个数据进行动态路由的设置。如果没有将数据存放在localStorage或者localStorage中的数据改变以后那么菜单就会对应改变。

问题就出在localStorage里面。如果设置动态路由是根据state.menuList去设置,而且对state.menuList进行了更改,那么menuList自然就变了,而且localStorage里面的数据也变了。再到页面上通过动态路由去跳转页面自然找不到了。所以就会报警告。其实跟设置标识与否无关(这个标识其实和下面代码的checkRouter作用一样的)

1.登录后获取菜单数据,并且设置到vuex中,vuex里面需要调用localStorage进行本地数据持久化

    // 获取菜单栏数据
    let result = await getMenuList(formLogin);
  
    // 动态设置menu
    await store.commit("layout/addMenuList", result.data);
    
    //    动态添加路由
    await store.commit("layout/setDynamicRoutes", router);

2.路由全局守卫中,需要再次设置动态路由,且判断路由是否存在,存在直接跳转

// 路由守卫中再次设置路由(此时是在地址栏直接回车时处理)
store.commit("layout/setDynamicRoutes", router);
// 判断路由在配置的动态路由中是否存在,不存在则跳转到home,存在则直接跳转
const checkRouter = (path) =>{
  let hasCheck =  router.getRoutes().filter(item=>item.path == path).length;
  if (hasCheck) {
    return true
  } else {
    return false
  }
}

router.beforeEach(async (to, from, next) => {
  // 要重新获取token
  const token = store.state.user.token;

  //注意这里的逻辑: 如果咩有token且路由不为/login则直接跳转到login页面
  // 如果路由存在且有token则直接到home页面否则直接next
  if (!token && to.path !== '/login') {
    next('/login');
    // 否则检查路由是否存在,不存在直接跳转home页面
  }else if(!checkRouter(to.path)){
      next('/main');
  }else {
    next();
  }
});

3.动态路由设置方法——这个方法是写在vuex中的

async setDynamicRoutes(state, router) {
        // 循环menuList设置动态路由
        let menuList = JSON.parse(JSON.stringify(state.menuList));
        
        // 通过menuList给routes循环添加数据
        let menuRoutes = getMenuRoutes(menuList);

        // 通过import.meta.glob获取模块化文件,类似与webpack的require.context()
        // ../../views/home/Home.vue: () => import("/src/views/home/Home.vue")
        const modules = import.meta.glob("../../views/**/*.vue");
        
        menuRoutes.length>0 && menuRoutes.map(route=>{
            let url = `../../views${route.path}/${route.component}.vue`
            route.component = modules[url];
            // 添加动态路由
            router.addRoute('home', route)
        })
    }
const getMenuRoutes = (menuList) =>{
    let routes = [];
    const deepList = (list) =>{
        while(list.length){
            // 删除最后一个
            let item = list.pop();
            // 有action表示是最底层子级菜单
            if(item.action){
                routes.push({
                    name: item.menuName,
                    path: item.path,
                    component: item.component,
                    meta: {
                        title: item.menuName
                    }
                });
            }

            // 如果有children说明还有子级,需要循环处理子级菜单
            if(item.children && !item.action){
                deepList(item.children)
            }
        }
    }
    deepList(menuList)

    return routes;
}

  • 3
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Vue.js 是一款前端 JavaScript 框架,它可以帮助开发者快速构建现代化的 Web 应用程序。在 Vue.js 中,动态路由是一种在运行时动态生成路由的方式,通常用于实现权限管理。 在 Vue.js 中,我们可以使用路由守卫来实现权限管理路由守卫是在路由切换前或切换后执行的函数,我们可以在这些函数中检查用户是否有权限访问该路由。如果用户没有权限访问该路由,我们可以将其重定向到其他页面,或者显示一个错误页面。 下面是一个使用动态路由路由守卫实现权限管理的示例: ``` import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) const router = new Router({ routes: [ { path: '/', name: 'home', component: () => import('@/views/Home.vue') }, { path: '/admin', name: 'admin', component: () => import('@/views/Admin.vue'), meta: { requiresAuth: true } }, { path: '/login', name: 'login', component: () => import('@/views/Login.vue') }, { path: '*', name: 'notFound', component: () => import('@/views/NotFound.vue') } ] }) router.beforeEach((to, from, next) => { const requiresAuth = to.matched.some(record => record.meta.requiresAuth) const isAuthenticated = localStorage.getItem('token') if (requiresAuth && !isAuthenticated) { next('/login') } else { next() } }) export default router ``` 在上面的代码中,我们定义了三个路由:home、admin 和 login。其中,admin 路由需要用户登录才能访问,因此我们在其 meta 中添加了 requiresAuth 属性。 在 router.beforeEach 函数中,我们检查用户是否已经登录,如果没有登录且要访问的路由需要登录,则将其重定向到登录页面。如果已经登录,则允许用户访问该路由。 在实际开发中,我们通常需要从后端获取用户的权限信息,并根据权限动态生成路由。这可以通过在路由守卫中动态添加路由来实现。具体实现可以参考 Vue.js 官方文档中的动态路由章节。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值