vue 权限管理(vue3 一个基于pinia简单易懂的系统权限管理实现方案,vue-router动态路由异步问题解决)
前情提要
作为项目经验稀少的vue开发者来说 ,在关键技术点上的经验不多 ,我希望通过我的思想和实践 ,把好的东西分享在这里 ,目的是进一步促进技术交流 。项目即将完成 ,权限是最后的收尾工作 ,好的权限实现方案 ,可以让我们没有后顾之忧 ,也可以提升项目的运行速度 。
应用场景
在开发之前 ,我粗略的浏览了一些权限实现方法,可以说智者见智吧 ,例如一种实现方案是在router的守卫里判断 ,我认为虽然实现了功能,但是增加了路由的功能压力。我们的需求是登录后即获知权限 ,根据权限提供功能;根据以上俩点需求我做出了如下计划:
找一个合适的入口 ,判断要提供的菜单以及用户拥有什么权限,不能影响页面加载 在对应的页面中拿到权限 ,提供需要的功能 必须整个系统都可以轻易拿到 ,且要避免安全问题实战解析
大多的实现方案是添加路由 ,我也实践了一次 ,发现存在路由重定向问题 ,于是我准备反其道而行 ,初始提供所有路由 ,根据权限移除不需要的路由 ,上面我分析到 ,我不想给导航守卫添加过多的压力,避免影响页面渲染 ,所以我利用了pinia状态库+router.removeRoute来实现路由的控制;
1 、控制添加路由
首先我们应该准备基础的router配置 ,包括公共页面,异常页面等:
//router/index.js import { createRouter, createWebHistory } from vue-router import { useUsersStore } from "@/store/user"; import Cookies from "js-cookie"; import { isUserTime } from "@/tool/unitl.js" const routes = [ { path: /, name: Index, component: () => import(@/view/Index.vue), }, { path: /login, name: Login, component: () => import(@/view/Login.vue) }, { path: /business, name: Business, component: () => import(@/view/business/Index.vue) }, { path: /business/project, name: BusinessProject, component: () => import(@/view/business/Project.vue) }, { path: /commerce, name: Commerce, component: () => import(@/view/commerce/Index.vue) }, { path: /commerce/project, name: CommerceProject, component: () => import(@/view/commerce/Project.vue) }, { path: /consult, name: Consult, component: () => import(@/view/consult/Index.vue) }, { path: /consult/project, name: ConsultProject, component: () => import(@/view/consult/Project.vue) }, { path: /finance, name: Finance, component: () => import(@/view/finance/Index.vue), children: [ { path: projects/project, name: FinanceProjectsProject, component: () => import(@/view/finance/projects/Project.vue) }, { path: employee/project, name: FinanceEmployeeProject, component: () => import(@/view/finance/employee/Project.vue) }, { path: account, name: FinanceAccount, component: () => import(@/view/finance/account/Index.vue) }, { path: authority/project, name: FinanceAuthority, component: () => import(@/view/finance/authority/Project.vue) } ] }, { path: /chief, name: Chief, component: () => import(@/view/chief/Index.vue), children: [ { path: first_examine/project, name: firstExamineProject, component: () => import(@/view/chief/first_examine/Project.vue) }, { path: second_examine/project, name: secondExamineProject, component: () => import(@/view/chief/second_examine/Project.vue) } ] }, { path: /email, name: Email, component: () => import(@/view/email/Index.vue) }, { path: /person, name: Person, component: () => import(@/view/person/Detail.vue) }, { path: /:pathMatch(.*)*, name: 404, component: () => import(@C/tool/Page404.vue) } ] const router = createRouter({ history: createWebHistory(), //history routes }) router.beforeEach(async (to, from) => { //pinia仓库可以最早出现的地方 ,路由初始化完毕 const store = useUsersStore(); if (to.name == 404 && from.name == Login) { //处理404问题 ,并且从login跳转任何404页面重置为Login页面 return { name: Login }; } else if (store.isLogin) { //不是404,登录状态在 ,正常跳转 return true; } else if (isUserTime() && Cookies.get("loginPwd")) { //刷新进入 ,判断是否过期了登录时间 ,Cookie中是否存在密码 //如果存在免登录 ,如果登录过程存在路由不存在的问题则404 ,否则正常跳转 let exist = await store.Login(to); if (!exist) return { name: 404 }; } else return { name: "Login" }; }); export default router以上代码为router所有前期需要准备的代码 ,已经我处理路由异常情况的实现方案 ,核心是利用:
async await 俩个关键字实现 ,因为在登录后 ,我们配置移除菜单和权限是需要时间的,必须要严格控制异步 ,这里也顺便处理了路由输入回车跳转情况 , 通过await store.Login(to);传入了to的参数来判断如何跳转,接下来参考下我的pinia仓库实现过程; //src/store/user.ts import { defineStore } from pinia import { apiGetUser } from @/axios/user.js import CryptoJS from crypto-js import router from @/router/index.js // 第一个参数是应用程序中 store 的唯一 id export const useUsersStore = defineStore(users, { state: () => { return { isLogin: false, phone: sessionStorage.UserN ? CryptoJS.AES.decrypt( sessionStorage.getItem(UserN), "abc!" ).toString(CryptoJS.enc.Utf8) : , pwd: "", deptno: "", idCard: "", lockState: "", birthday: "", name: "", sex: "男", power: ,//权限:0-admin,1-部门总负责人,2-部门项目负责人,3-部门成员 showMenu: , } }, actions: { Login(to) { let _that = this; let MenuArray = [Business, BusinessProject, Commerce, CommerceProject, Consult, ConsultProject, Finance, Chief]; return new Promise((t, f) => { _that.isLogin = true; let data = new FormData(); data.append(loginAct, _that.phone) apiGetUser(data).then(res => { let { deptno, idCard, lockState, name, sex, birthday } = res, showMenu = deptno.charAt(0); console.log(res); _that.$patch({ deptno, idCard, lockState, name, sex, birthday, showMenu }) if (showMenu == R) { t(true) } else { //过滤菜单项 switch (showMenu) { case A: MenuArray = MenuArray.filter(item => { return item.indexOf(B) == -1 }); break; case B: MenuArray = MenuArray.filter(item => { return item.indexOf(Com) == -1 }); break; case C: MenuArray = MenuArray.filter(item => { return item.indexOf(Con) == -1 }); break; case D: MenuArray = MenuArray.filter(item => { return item.indexOf(F) == -1 }); break; case E: MenuArray = MenuArray.filter(item => { return item.indexOf(Ch) == -1 }); break; } //循环移除 let Length = MenuArray.length; MenuArray.forEach((item, index) => { if (index == Length - 1) { router.removeRoute(item); if(to.name && router.hasRoute(to.name)){ t(true) }else{ t(false) } } else { router.removeRoute(item) } }) } }) }) }, }, })这里我直接展示了我的代码 ,如果你不熟悉pinia,要抓紧补了 ,在用户登录验证成功后,这里会调用user库的login函数 ,也会携带路由要to的参数 ,这里只需要明确几个点即可:
MenuArray ,将来用来循环removeRoute使用的数组 ,映射路由name 根据自己的配置规则 ,筛选 MenuArray 通过index == Length - 1 严格控制最后一次移除 ,并且判断to.name是否合法 理解await原理 ,await必须出现在async函数中 ,且 返回函数如果是Promise ,则只接受resolved中的值; router, 熟悉vue3的同志知道 有一个useRouter 驱动函数,在我们的方案里不能那样使用 ,因为useRouter必须在setup函数中使用;2 、实践观察
我们的需求完美实现 ,接下来就是把不需要的菜单隐藏掉
3 、控制功能
路由我们已经控制住了,隐藏菜单就很容易了 ,加载userStore 获取条件v-if即可 ,例如在header组件中:
//header.vue import { useUsersStore } from "@/store/user.js" const user = useUsersStore(); //html <el-menu-item index="/">首页</el-menu-item> <el-menu-item v-if="AR.indexOf(user.showMenu) != -1" index="/business">业务部</el-menu-item> <el-menu-item v-if="BR.indexOf(user.showMenu) != -1" index="/commerce">商务部</el-menu-item> <el-menu-item v-if="CR.indexOf(user.showMenu) != -1" index="/consult">咨询部</el-menu-item> <el-menu-item v-if="DR.indexOf(user.showMenu) != -1" index="/finance">财务部</el-menu-item> <el-menu-item v-if="ER.indexOf(user.showMenu) != -1" index="/chief">总工办</el-menu-item>这边根据自己团队的权限规范条件处理即可,其他的控制举一反三
4 、解决异步路由问题
router.beforeEach(async (to, from) => { //pinia仓库可以最早出现的地方 ,路由初始化完毕 const store = useUsersStore(); if (to.name == 404 && from.name == Login) { //处理404问题 ,并且从login跳转任何404页面重置为Login页面 return { name: Login }; } else if (store.isLogin) { //不是404 ,登录状态在 ,正常跳转 return true; } else if (isUserTime() && Cookies.get("loginPwd")) { //刷新进入 ,判断是否过期了登录时间 ,Cookie中是否存在密码 //如果存在免登录 ,登录过程存在路由不存在问题则404 ,否则正常跳转 let exist = await store.Login(to); if (!exist) return { name: 404 }; } else return { name: "Login" }; });这里是我项目的路由守卫 ,它已经完成绝大部分功能,登录权限判断 ,404处理 ,刷新进入,状态持久化 ,异步操作在网络环境中是无法避免的 ,router官方示例也明确提醒了我们如何使用,这里我们是利用服务器数据判断要移除的菜单 ,且不说http响应异步 ,router.removeRoute()也只能是一次删除一个 ,所以在store.Login(to)我严格控制了逻辑 ,并且也解决了上述问题,使用我这样的全局守卫来配置必须要理清楚逻辑:依靠 async及await来实现 ,缺点是会有短暂的延迟 ,还不够流畅 ,但是笔者由于时间问题 ,不能再优化下去 ,希望大家吸取精华去掉糟粕 。
至此我的分享结束,如果大家有更好的解决方案 ,希望可以进一步交流 。
最后
📚 vue专栏
☃️ 个人简介:一个喜爱技术的人 。
🌞 励志格言: 脚踏实地 ,虚心学习 。
❗如果文章还可以,记得用你可爱的小手点赞👍关注✅ ,我会在第一时间回 、回访 ,欢迎进一步交流 。创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!