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

news2024/9/24 1:26:38

前言

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

阅读本文您将收获:

  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-menu
        class="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 路由权限控制 是个老生常谈的话题,只要是业务系统,必然会涉及到权限问题,想必大家对此也有多种多样的解决方案和实现方式,非常欢迎大家评论区讨论😋。您的任何反馈我都会仔细斟酌,感谢万分!!!

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1645131.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Q1保健品线上市场分析(一):护眼需求不减,叶黄素软糖卖爆!

如今网络时代的高速发展&#xff0c;用眼过度逐渐成为国人的常态&#xff0c;无论是儿童还是青少年、成年人、老年人&#xff0c;都可能面临眼部健康的问题。 在这样的大环境下&#xff0c;预防大于治疗的概念不断深入日常生活&#xff0c;进而推动了护眼产品市场的高速发展&a…

1W 3KVDC 隔离 稳压单输出 DC/DC 电源模块 ——TPV-SAR 系列

TPV-SAR系列产品是专门针对PCB上分布式电源系统中需要与输入电源隔离且输出精度要求较高的电源应用场合而设计。该产品适用于&#xff1b;1&#xff09;输入电源的电压变化≤5%&#xff1b;2&#xff09;输入输出之前要求隔离电压≥3000VDC&#xff1b;3&#xff09;对输出电压…

python学习笔记-01

python 在学习之前要了解的事项&#xff1a; 1.python缩进语法要求较为严格 2.是解释型语言 3.python2版本和python3版本不兼容 本系列笔记全部基于python3 1.hello world 安装好python之后&#xff0c;可以直接打开python&#xff0c;也可以通过cmd进入python。 print(&qu…

【如此简单!数据库入门系列】之存储设备简介

文章目录 1 前言2 存储设备分类3 主存层次结构4 磁盘结构5 RAID6 总结7 系列文章 1 前言 没有存储&#xff0c;就没有数据&#xff01; 如果说ER模型和数据库规范化是数据库概念模式的技术和方法&#xff0c;那么存储设备就是数据库物理模式的基础。 物理存储设备包含哪些类型…

河南大学大礼堂火灾事故引发安防监控对智能分析技术应用的思考

一、方案背景 2024年5月2日&#xff0c;在修缮施工期间的河南大学河南留学欧美预备学校旧址大礼堂发生火情。现场航拍画面显示&#xff0c;大礼堂经过火灾&#xff0c;房顶已经基本坍塌&#xff0c;被火烧过的建筑呈焦黑状。 公开资料显示&#xff0c;大礼堂属河南留学欧美预…

小程序激励广告视频多次回调问题

1.问题 2. 激励视频使用及解决方案 官方文档 let videoAd null; // 在页面中定义激励视频广告 Page({/*** 页面的初始数据*/data: {},/*** 生命周期函数--监听页面加载*/onLoad(options) {let that this;// 创建激励视频广告实例if (wx.createRewardedVideoAd) {videoAd w…

Docker镜像仓库-在私有镜像仓库推送或拉取镜像

推送镜像到私有仓库&#xff0c;要先让镜像打包 前缀为私有仓库地址的名字&#xff1a; 这里也是打包成功了:docker images 可以查看到 push推送镜像到镜像仓库: docker push 192.168.221.129:8080/nginx:1.0推送成功后在主机访问镜像仓库可以看到 这里已经有个镜像了。而且可…

从线索到成交:HubSpot全渠道销售流程的精准打磨

在当今数字化时代&#xff0c;企业面临着越来越复杂的市场环境和激烈的竞争压力。在这样的背景下&#xff0c;如何利用科技手段提升营销和销售效率&#xff0c;拓展客户获客渠道&#xff0c;成为了企业发展的关键之道。作为HubSpot合作伙伴&#xff0c;我们深谙全渠道营销与销售…

屏幕距离识别加语音提醒

效果 QQ录屏20240406131651 语音播放 import pygame pygame.mixer.init() #初始化 pygame.mixer.music.load(r"7359.wav") #文件位置 pygame.mixer.music.play(1) #播放语音 while pygame.mixer.music.get_busy(): # 在音频播放为完成之前不退出程序pass 完…

im即时通讯源码/仿微信app源码+php即时通讯源码带红包+客服+禁言等系统php+uniapp开发

即时通讯(IM)系统是现代互联网应用中不可或缺的一部分&#xff0c;它允许用户进行实时的文本、语音、视频交流。随着技术的发展&#xff0c;IM系统的功能越来越丰富&#xff0c;如红包、客服、禁言等。本文将探讨如何使用PHP语言开发一个功能完备的即时通讯系统&#xff0c;包括…

git commit 提交报错pre-commit hook failed (add --no-verify to bypass) 解决方法,亲测有效

问题截图 今天在执行 git commit 命令时报错&#xff1a;pre-commit hook failed (add --no-verify to bypass) 解决 参考文章&#xff1a;git commit报错&#xff1a;pre-commit hook failed的解决方法 具体原理什么的就不解释了&#xff0c;可以看看上面的参考文章 解决方…

《Fundamentals of Power Electronics》——脉宽调制器建模

下图给出了一个简单脉宽调制器电路的原理图。 脉宽调制器电路产生一个用于指令转换器功率管导通和关断的逻辑信号δ(t)。该逻辑信号δ(t)是周期性的&#xff0c;其频率为fs&#xff0c;占空比为d(t)。脉宽调制器的输入是一个模拟控制信号vc(t)。脉宽调制器的作用是产生一个与模…

开源!自制一个桌面宠物(STM32CUBEMX HAL库 PWM波 小项目)

原视频&#xff1a;好&#xff0c;自制一个桌面宠物&#xff01;_哔哩哔哩_bilibili 基础所需&#xff1a;基础电路认识&#xff0c;C语言&#xff0c;STM32开发&#xff0c;STM32CUBEIDE或CUBEMX和Keil使用&#xff08;重要&#xff09;&#xff0c;一点点艺术细胞、一点点耐…

前端之深拷贝

前提&#xff1a; 就是在实际开发中&#xff0c;我有一个编辑的弹窗&#xff0c;可以查看和编辑&#xff0c;因为弹窗里面是一个步骤条&#xff0c;点击下一步就要向对应的接口发送请求&#xff0c;考虑到就比如我点击下一步&#xff0c;此次表箱信息其实不需要修改&#xff0…

Linux —— 信号(2)

Linux —— 信号&#xff08;2&#xff09; 信号的序号kill系统调用不可被自定义的信号raiseabort 异常传递信号SIGFPESIGSEGV alarm传递信号 我们今天来接着了解信号&#xff1a; 信号的序号 我们来看看信号的序号&#xff1a; 信号的序号是从1开始&#xff0c;到31我们称这…

为什么要梯度累积

文章目录 梯度累积什么是梯度累积如何理解理解梯度累积梯度累积的工作原理 梯度累积的数学原理梯度累积过程如何实现梯度累积 梯度累积的可视化 梯度累积 什么是梯度累积 随着深度学习模型变得越来越复杂&#xff0c;模型的训练通常需要更多的计算资源&#xff0c;特别是在训…

【热门话题】ElementUI 快速入门指南

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 ElementUI 快速入门指南环境准备安装 ElementUI创建 Vue 项目安装 ElementUI 基…

Android selinux权限

一.SE 概述 SELinux 是由美国NSA&#xff08;国安局&#xff09;和 SCC 开发的 Linux的一个扩张强制访问控制安全模块。原先是在Fluke上开发的&#xff0c;2000年以 GNU GPL 发布。从 fedora core 2开始&#xff0c; 2.6内核的版本都支持SELinux。 在 SELinux 出现之前&#…

开源模型应用落地-CodeQwen模型小试-小试牛刀(一)

一、前言 代码专家模型是基于人工智能的先进技术&#xff0c;它能够自动分析和理解大量的代码库&#xff0c;并从中学习常见的编码模式和最佳实践。这种模型可以提供准确而高效的代码建议&#xff0c;帮助开发人员在编写代码时避免常见的错误和陷阱。 通过学习代码专家模型&…

百度语音识别开发笔记

目录 简述 开发环境 1、按照官方文档步骤开通短语音识别-普通话 2、创建应用 3、下载SDK 4、SDK集成 5、相关接口简单说明 5.1权限和key 5.2初始化 5.3注册回调消息 5.4开始转换 5.5停止转换 6、问题 简述 最近想做一些语音识别的应用&#xff0c;对比了几个大厂…