项目实战 | 如何恰当的处理 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

相关文章

如何使用celery进行异步处理和定时任务(django)

《如何使用celery进行异步处理和定时任务(django)》文章介绍了Celery的基本概念、安装方法、如何使用Celery进行异步任务处理以及如何设置定时任务,通过Celery,可以在Web应用中... 目录一、celery的作用二、安装celery三、使用celery 异步执行任务四、使用celery

Linux中chmod权限设置方式

《Linux中chmod权限设置方式》本文介绍了Linux系统中文件和目录权限的设置方法,包括chmod、chown和chgrp命令的使用,以及权限模式和符号模式的详细说明,通过这些命令,用户可以灵活... 目录设置基本权限命令:chmod1、权限介绍2、chmod命令常见用法和示例3、文件权限详解4、ch

SpringBoot操作spark处理hdfs文件的操作方法

《SpringBoot操作spark处理hdfs文件的操作方法》本文介绍了如何使用SpringBoot操作Spark处理HDFS文件,包括导入依赖、配置Spark信息、编写Controller和Ser... 目录SpringBoot操作spark处理hdfs文件1、导入依赖2、配置spark信息3、cont

Golang使用minio替代文件系统的实战教程

《Golang使用minio替代文件系统的实战教程》本文讨论项目开发中直接文件系统的限制或不足,接着介绍Minio对象存储的优势,同时给出Golang的实际示例代码,包括初始化客户端、读取minio对... 目录文件系统 vs Minio文件系统不足:对象存储:miniogolang连接Minio配置Min

javafx 如何将项目打包为 Windows 的可执行文件exe

《javafx如何将项目打包为Windows的可执行文件exe》文章介绍了三种将JavaFX项目打包为.exe文件的方法:方法1使用jpackage(适用于JDK14及以上版本),方法2使用La... 目录方法 1:使用 jpackage(适用于 JDK 14 及更高版本)方法 2:使用 Launch4j(

Node.js 中 http 模块的深度剖析与实战应用小结

《Node.js中http模块的深度剖析与实战应用小结》本文详细介绍了Node.js中的http模块,从创建HTTP服务器、处理请求与响应,到获取请求参数,每个环节都通过代码示例进行解析,旨在帮... 目录Node.js 中 http 模块的深度剖析与实战应用一、引言二、创建 HTTP 服务器:基石搭建(一

Docker集成CI/CD的项目实践

《Docker集成CI/CD的项目实践》本文主要介绍了Docker集成CI/CD的项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录一、引言1.1 什么是 CI/CD?1.2 docker 在 CI/CD 中的作用二、Docke

React实现原生APP切换效果

《React实现原生APP切换效果》最近需要使用Hybrid的方式开发一个APP,交互和原生APP相似并且需要IM通信,本文给大家介绍了使用React实现原生APP切换效果,文中通过代码示例讲解的非常... 目录背景需求概览技术栈实现步骤根据 react-router-dom 文档配置好路由添加过渡动画使用

SpringBoot项目引入token设置方式

《SpringBoot项目引入token设置方式》本文详细介绍了JWT(JSONWebToken)的基本概念、结构、应用场景以及工作原理,通过动手实践,展示了如何在SpringBoot项目中实现JWT... 目录一. 先了解熟悉JWT(jsON Web Token)1. JSON Web Token是什么鬼

手把手教你idea中创建一个javaweb(webapp)项目详细图文教程

《手把手教你idea中创建一个javaweb(webapp)项目详细图文教程》:本文主要介绍如何使用IntelliJIDEA创建一个Maven项目,并配置Tomcat服务器进行运行,过程包括创建... 1.启动idea2.创建项目模板点击项目-新建项目-选择maven,显示如下页面输入项目名称,选择