项目实战 | 如何恰当的处理 Vue 路由权限

2024-05-07 14:52

本文主要是介绍项目实战 | 如何恰当的处理 Vue 路由权限,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

哈喽,小伙伴你好,我是 嘟老板。最近接了一个成本千万级的前端项目运维工作,本着 知己知彼 的态度,我将整个前端的大致设计思路过了一遍。不看不知道,一看…吓一跳。光是 路由权限 这块儿的设计,都让我“受益匪浅”,特写此篇文章来记录自己的所学所感。

阅读本文您将收获:

  1. 真实 vue 项目的路由权限处理思路及伪代码实现(反面案例)。
  2. 个人对于 vue 路由权限处理的设计思考及伪代码实现。
  3. 结合您个人项目经验,思考更优的 vue 路由权处理方案,评论区讨论。

场景分析

接触过业务系统开发的小伙伴应该知道,对于前端来说一个比较重要的事情:权限控制。当然这并不是说系统的权限会交由前端处理,而是应该在后端提供正确的权限数据的前提下,前端合理的做到用户操作上的权限控制。

前端权限控制大致可以分为三类:

  • 菜单权限:用于可以访问哪些菜单。
  • 操作权限:用户可以执行哪些操作,如增删改查,导入导出等。
  • 路由权限:配合菜单权限,从路由上规避用户越权访问的行为。

本文的内容假设在菜单权限正确提供的前提下,路由权限 该如何处理?

关于完整的权限控制,后续会单独出一篇系统性的文章,敬请期待…

路由权限 顾名思义,就是管理前端路由的。对于 Vue 项目来说,就是对于 vue-router 的一系列处理。

真实业务场景中,后端会提供权限查询接口(通常是菜单接口),前端调用来获取当前用户已分配的所有菜单数据。前端根据给定的菜单数据,通过一系列手段,实现对于用户访问的权限控制。

项目案例

路由权限处理的思路很简单,但落地的实现方案却可以多种多样,我们来看看案例项目是如何做的?是否合理?又存在哪些问题呢?

大致分为以下 3 步:

  1. 配置前端路由。
  2. 应用入口获取菜单数据。
  3. 根据菜单数据,控制左侧菜单栏展示内容,实现访问入口控制。

1. 配置前端路由

src/router/index 文件中维护了一份完整的路由配置,并将该配置数组传给 createRouter 创建路由对象。

// 路由配置
const routes = [{path: '/404',component: () => import('@/views/error-page/404.vue')},{path: '/401',component: () => import('@/views/error-page/401.vue')},// 其他所有业务页面路由配置...
]// 创建路由对象
const router = createRouter({history: createWebHashHistory(),routes: routes
})export default router

通过以上代码创建了路由实例 router 并导出,然后在应用的入口文件,一般是 src/main 文件中,引入并安装。

import { createApp } from 'vue'
import router from '@/router'
import App from './App.vue'const app = createApp(App)app.use(rotuer)

2. 应用入口获取菜单数据

封装获取菜单数据的方法,本项目中使用 Vuex 管理菜单数据,作为全局状态。在 src/store/modules 目录下新建 menus 文件,并维护菜单相关的 statemutationsactions 等,其中定义了 getMenus action 用来获取参数数据。

export default {namespaced: true,state: () => ({menus: [], // 菜单数据// ...}),mutations: {SET_MENUS(state: any, menus: any []) {state.menus = menus},// ...},actions: {getMenus({commit, state}) {return new Promise((resolve, reject) => {getAuthMenu().then((res: any) => {const sortMenus = sortRoutes(res)commit('SET_MENUS', sortMenus)// 其他处理逻辑...resolve(sortMenus)}).catch((err: any) => {reject(err)})})},}
}

然后在 src/main 文件中通过调用 store.dispatch 来触发 getMenus 以获取菜单数据逻辑。

await store.dispatch('menu/getMenus')

3. 处理左侧菜单栏

上一步已经将菜单数据存储到全局状态里了,接下来我们将菜单数据绑定到左侧菜单栏组件,即可渲染最终的可访问菜单了。

<template>
<!-- 其他页面结构 ...--><el-complete-menuclass="navigator-menu":data="menuData":collectable="true":collectedData="collectMenus":common-used="false"@collect="handleCollect"@select="onMenuItemSelected"@collapseChange="onMenuCollapseChange"/>
<!-- 其他页面结构 ...-->
</template>
<script setup lang="ts">
import { useStore } from 'vuex'const $store = useStore()
const menuData = ref($store.state.menu.menus)
</script>

el-complete-menu 是基于 el-menu 封装的菜单栏组件,data 即为需要展示的菜单树形结构。

大致的实现流程如上,不需要过分纠结代码实现的细节,全部代码远比上面复杂得多。

了解了案例项目的实现过程,接下来请你先暂停并思考下最开始的问题,这个实现方案是否合理?又存在哪些问题?带着问题,我们走进下一节…

优化方案

前端可以通过配置 vue-router 来实现页面导航,那么实现路由权限的核心思路,就是根据给定的权限数据,调整 vue-router 的配置项

再来看下案例项目的问题:

  1. 没有做到完全的路由权限控制

    我们回顾下 router 部分的代码,在 router/index 文件中维护了一份完整的路由配置数组,然后用其来创建 router 实例。 这里最明显的问题,就是没有根据菜单权限数据来处理 router 配置项。这就可能导致一个现象:虽然左侧菜单栏权限控制住了,但是用户可以直接通过 url,打开他并没有权限访问的页面。

  2. 权限获取时机问题,导致入口文件代码量激增

    案例项目将获取菜单的逻辑代码,写到了应用入口文件 main 中,功能上没有问题,确实能够实现启动应用即获取权限数据。但是对于入口文件来说,首先要保持逻辑的精简,尽可能仅负责应用相关的逻辑处理,如创建应用对象安装插件(VueRouter、Vuex 等)国际化等。除此之外,若掺杂了业务数据获取逻辑在内,首先会导致文件内容混杂,关注点不突出;

明确了问题点,我们就来考虑下该如何处理?

  1. VueRouter 配置调整,将一个完整的路由配置数组拆分为固定配置权限配置。其中固定配置是指不需要通过权限来分配,每个人都可以访问的路由,如 主页错误页面(403、404等)权限配置即需要分配权限才能访问的路由,一般是业务模块。
  2. 使用固定配置创建 router 实例,保证固定配置中的路由始终可以访问,而权限配置的路由则要按照权限数据动态添加。
  3. VueRouter 的全局前置守卫 beforeEach 中初始化权限菜单及动态添加路由配置,解放 main,也能保证每次进入应用即初始化。

大体流程如下:

image.png

实现步骤

了解了大致步骤,我们来逐步实现一下:

1. VueRouter 配置,创建 VueRouter 实例并导出

我们需要定义两个路由配置:staticRoutesdynamicRoutes,即 静态路由动态路由

src/router/index 文件中定义:

const staticRoutes = [{name: 'root',path: '/',redirect: import.meta.env.VITE_ROUTE_HOME_PATH,meta: {title: '主页'}},{name: '404',path: '/404',component: RouteComponents[404],meta: {title: '未找到'}},{name: 'not-found',path: '/:pathMatch(.*)*',component: 'blank',meta: {title: '未找到'}}// ...
]export const dynamicRoutes = [{name: 'UserManage',path: 'user-manage',component: () => import('@/views/system-manage/user-manage/index.vue'),meta: { title: '用户管理', keepAlive: true }},// ...
]

其中 静态路由 用来定义 router 实例,保证其配置的页面一直可访问。

const router = createRouter({history: createWebHashHistory(),routes: staticRoutes,strict: true,sensitive: true
})// 安装 router 插件的方法
export async function setupRouter(app: App<Element>) {app.use(router)setupRouterGuards(router)// 等待路由初始化完成await router.isReady()
}export default router

注:在 router/idnex 中导出了一个 setupRouter 函数,用来安装 router 插件和添加路由守卫,用于在应用入口 mian 文件中调用。

动态路由 作为备用,在 权限处理 部分会派上用场。

2. 创建应用,安装 router 插件

在应用的入口文件 main 中创建应用实例

// 创建应用实例
const app = createApp(App)// 安装路由,添加路由守卫
await setupRouter(app)// 挂载 到 id 为 app 的 dom 节点下
app.mount('#app')

3. 添加路由导航守卫 beforeEach

第一步在 router/index 文件中有一个 setupRouter 函数,其中调用了一个方法 setupRouterGuards,该方法就是用来添加路由导航的。现在我们来看下它都干了些什么事情?

export async function setupRouterGuards(router: Router) {// 添加前置导航守卫const beforeEachGuard = await createBeforeEachGuard()router.beforeEach(beforeEachGuard)// 添加后置导航守卫const afterEachGuard = await createAfterEachGuard()router.afterEach(afterEachGuard)
}

内容不多,就是添加了两个全局导航守卫,本文中我们只需要关注前置导航守卫即可,那么我们继续深入 createBeforeEachGuard 内部瞅瞅。

export default function createBeforeEachGuard(): NavigationGuardWithThis<undefined> {return async (to: RouteLocationNormalized,from: RouteLocationNormalized,next: NavigationGuardNext) => {// 处理动态路由权限相关,若返回 true,则继续往下执行 const normalNext = await handleDynamicRoutes(to, from, next)normalNext && next()}
}async function handleDynamicRoutes(to: RouteLocationNormalized,from: RouteLocationNormalized,next: NavigationGuardNext
) {const menuStore = useMenuStore()// 是否已初始化权限菜单const isAuthInitialized = menuStore.isAuthInitialized// 若未初始化,则执行初始化函数if (!isAuthInitialized) {await menuStore.initAuthMenus()// 其他处理逻辑...}return true
}

处理逻辑都在 handleDynamicRoutes 中,首先校验是否已经初始化过权限,即 isAuthInitialized。若未初始化,执行 menuStore.initAuthMenus 函数。那么 menuStore.initAuthMenus 都干了些什么呢,我继续往下看。

4. 动态处理路由配置

终于来到最核心的部分了,上一步我们看到了 menuStore.initAuthMenus 函数,现在我们进入该函数内部瞅一瞅。

const initAuthMenus = async () => {// 获取菜单权限列表const menus = await getMenuList()// 初始化路由权限配置await routeStore.initRoutes(menus)// 将平铺的路由数组转换为属性结构,该结果用于渲染菜单栏authMenus.value = transformFlatMenusToTree(menus)// 执行完成后,将 `isAuthInitialized` 置为 true,避免页面跳转时重复执行权限处理逻辑isAuthInitialized.value = true
}

可以看到,initAuthMenus 主要是获取权限菜单数据,并根据菜单数据处理路由配置(routeStore.initRoutes 函数的逻辑);由于后端的设计是返回数组形式,需要前端将数组转化为树形结构,供菜单栏使用(transformFlatMenusToTree 函数的逻辑)。

现在我们进入 initRoutes 函数:

const initRoutes = async (menus: ZiMuAuth.Menu[]) => {if (!routes.length) return// 将 路由配置 转化为可用的、平铺的 vue 路由const vueRoutes = transformRouteConfigToVueRoutes(routes)// 根据权限菜单数据匹配路由配置const matchedRoutes = matchRoutesByAuthMenus(vueRoutes, menus)for (const route of matchedRoutes) {router.addRoute(route)}
}export function matchRoutesByAuthMenus(routes: RouteRecordRaw[],menus: ZiMuAuth.Menu[]
) {if (!menus.length || !routes.length) return []const menuCodes = menus.map(m => m.code)const matchRoutes = (routes: RouteRecordRaw[]) => {const target: RouteRecordRaw[] = []for (const route of routes) {const matched = !route.name || menuCodes.includes(route.name as string)if (matched) {if (route.children?.length) {route.children = matchRoutes(route.children)}target.push(route)}}return target}const result: RouteRecordRaw[] = matchRoutes(routes)return result
}

对于路由配置的处理在 matchRoutesByAuthMenus 函数中,以递归的方式,拿路由配置的 name 属性与菜单的 code 进行匹配,若父菜单匹配通过,则继续匹配其子菜单,直到所有路由配置全部匹配完成。

然后遍历匹配通过的路由配置,调用 router.addRoute 函数添加到路由中。

5. 整完收工

前端对于路由权限的处理到这就告一段落了,主要是梳理下整体的实现思路,小伙伴们不需要太过关注代码的实现细节,正如 一千个读者就有一千个哈姆雷特,每个小伙伴写代码的习惯、组织方式、认知程度都有所不同,最终成品肯定也大相径庭,优秀的你肯定比我做的要更好!!!

相关代码已上传 GitHub,感兴趣的小伙伴可以去瞅瞅

关于路由权限的其他思考

对于程序猿来说,懂得思考是美德,能力的退步将从停止思考开始。

除了上面的处理方案,还有没有其他的可行性方式?

现在我们以硬编码的方式将所有路由配置维护在代码里,然后再根据权限菜单动态添加到路由中。既然有动态处理的步骤,那如果不在代码里维护,直接动态生成配置是否可行呢?

可行,需要结合权限模块的设计,将路由需要的必要属性添加到菜单配置中,比如 路由地址对应组件 等。不过这会将权限配置与前端开发紧密耦合,不见得是更优解,但却也是一个不错的思路,毕竟权限模块的设计总是要或多或少的考虑代码逻辑,只是个耦合程度的问题。

后续会考虑将这块添加到个人工程 中,感兴趣的小伙伴可以关注一下…

结语

好啦,今天的内容就到这里了。vue 路由权限控制 是个老生常谈的话题,只要是业务系统,必然会涉及到权限问题,想必大家对此也有多种多样的解决方案和实现方式,非常欢迎大家评论区讨论😋。您的任何反馈我都会仔细斟酌,感谢万分!!!

感谢阅读,愿 你我共同进步,谢谢!!!

这篇关于项目实战 | 如何恰当的处理 Vue 路由权限的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/967665

相关文章

mybaits参数处理map-------mybatis(五)

mybaits参数处理 Mybatis日常业务总我们无论是查询也好,插入也罢,我们一般都需要将实体的多个属性值到映射的sql语句中,比较常见的做法就是将实体属性封装到Map中,mybatis会自动从map中取出对应的属性值构造sql语句。下面为实例代码: 实体类 package model;public class Bloger {private int id;private Str

vue3显示element-plus所有icon

效果 代码 <template><div style="display: flex;flex-wrap: wrap"><component :is="name" style="width: 2rem; margin-left: 2rem" v-for="(name,index) in icons" :index="index" :key="index"></component></div>

bimface开发实战-vue版

效果 演示地址 框架 bimface + vue3.0 代码地址 gitee地址 使用 yarn install yarn serve

vue3.0 v-model 的使用

前言 组件功能:把 el-switch 的值 false/true, 动态绑定输出为 0, 1 组件代码 封装el-switch组件,当el-switch的值为false,输出值为0;当el-switch的值为true,输出值为1; <template><el-switch v-model="switchValue" @change="changeEvent"></el-switch

项目总结一:微孵化项目总结

一、项目简介 1、项目基本信息 基本信息详细项目名称微孵化平台iOS(兼容iOS 7.0 以上的系统)V 1.0开发周期2015年8月6日-2015年9月9日(共24天,除去周末,无加班)项目背景此次项目名称叫做微孵化,是为为中小微企业发展全周期(种子期、初创期、成长期、成熟期)提供数十项专业服务,提供公司注册、场地租赁、人才服务、法务咨询、投融资等多项专业化服务。主要技术1、广告轮播;2、微

SpringBoot 学习二:项目属性配置

默认配置的端口是8080,但是有时候8080端口会被系统其他程序占用,所以我们最好配置一个不常用的端口。 1、配置端口 在resources目录下新建一个application.yml文件 打开applicationl.yml文件,里面是空白的,在里面添加端口: server:port: 8081 重新运行一下程序,在浏览器里面将端口号改成8081,能访问成功,则说明设置生效了。

Android JNI开发五: JNI字符串的处理

JNI 字符串处理 1. NewStringUTF 创建字符串 jstring NewStringUTF(const char *utf) {return functions->NewStringUTF(this,utf);}参数说明:-utf: 字符串说明:通过调用NewStringUTF函数,会构建一个新的java.lang.String字符串对象。这个新创建的字符串会自动转换成Java

Vue学习笔记:拦截器

原文地址 Vue可以对http request和http response添加全局拦截,最典型的例子就是在请求头里添加token,和监测是否登录,如果没有登录则跳转到登录页面。 main.js中添加拦截器的代码: 1. request 拦截器 //request 拦截器,在请求头中加tokenaxios.interceptors.request.use(config => {if (lo

Sql Server 对用户权限的授予:Grant、拒绝Deny、收回Revoke

你是否有遇到这种问题: 1.程序出错:PermissionDineDateAccessExcetion 2.Cause: com.microsoft.sqlserver.jdbc.SQLServerException:拒绝了对象”user”的select.insert权限 1.对表增删改查授权 对用户授权,允许其具有对数据表user的更新和删除的操作权限: GRANT UPDATE,DE

Android Studio项目svn文件忽略设置

前言 在Android studio项目中总有文件和目录不需要进行版本控制。下面是在As项目中设置忽略文件的具体操作方法和一般需要忽略的文件和文件夹。 需要忽略的文件 .idea 文件夹.gradle 文件夹所有的 build 文件夹所有的 .iml 文件local.properties 文件 具体操作方法 Settings --> Version Control --> Ignored