Vue 动态路由权限控制:解决刷新路由丢失问题

在现代前端应用中,基于角色的权限控制是必不可少的功能。本文将详细讲解如何在 Vue 项目中实现动态路由权限控制,并解决刷新页面后路由丢失的常见问题。我们会逐一分析核心函数的作用和逻辑,同时提供两种刷新问题的解决方案,帮助你根据项目需求选择合适的实现方式

一、核心功能概述

动态路由权限控制的核心目标是:根据用户角色动态加载可访问的路由,并在页面刷新后保持路由状态。整个系统主要由以下几部分组成:

  • 路由配置(固定路由 + 动态路由)
  • 权限过滤工具函数
  • 状态管理(用户信息 + 路由加载状态)
  • 全局路由守卫(控制路由跳转和加载逻辑)
  • 侧边栏组件(动态渲染菜单)

下面我们将逐一解析每个部分的核心函数和实现逻辑,并重点讲解刷新问题的解决方案。

二、核心代码解析

1. 路由配置:区分固定路由与动态路由

路由配置是权限控制的基础,我们需要将路由分为 “无需权限的固定路由” 和 “需要权限的动态路由”。

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

// 固定路由(constantRoutes):所有用户都能访问的路由
export const constantRoutes = [
  {
    path: '/login',
    ***ponent: () => import('@/views/login.vue'),
    hidden: true // 登录页不在侧边栏显示
  },
  {
    path: '/',
    redirect: '/dashboard',
    hidden: true // 重定向路由不显示
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    ***ponent: () => import('@/views/dashboard.vue'),
    meta: { title: '仪表盘' } // 侧边栏显示的标题
  },
  {
    path: '/404',
    ***ponent: () => import('@/views/404.vue'),
    hidden: true // 404页不在侧边栏显示
  }
]

// 动态路由(asyncRoutes):需要权限控制的路由
export const asyncRoutes = [
  {
    path: '/admin',
    name: 'Admin',
    ***ponent: () => import('@/views/admin.vue'),
    meta: { 
      title: '管理员页面', // 侧边栏显示的标题
      roles: ['admin'], // 仅管理员可访问
      hidden: false // 在侧边栏显示
    }
  },
  {
    path: '/user',
    name: 'User',
    ***ponent: () => import('@/views/user.vue'),
    meta: { 
      title: '用户页面', // 侧边栏显示的标题
      roles: ['admin', 'user'], // 管理员和普通用户均可访问
      hidden: false // 在侧边栏显示
    }
  }
]

// 创建路由实例(初始只加载固定路由)
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: constantRoutes
})

export default router

核心逻辑

  • 固定路由:任何用户(包括未登录用户)都能访问,如登录页、首页等。
  • 动态路由:通过 meta.roles 定义访问权限,只有符合角色要求的用户才能访问。
  • hidden 属性:控制路由是否在侧边栏菜单中显示。

2. 状态管理:存储用户信息与路由状态

使用 Pinia 管理用户登录状态和路由加载状态,确保刷新页面后状态不丢失。

// src/store/index.js
import { defineStore } from 'pinia'

// 用户状态存储(useUserStore)
export const useUserStore = defineStore('user', {
  state: () => ({
    // 从本地存储获取token(刷新后仍能保持登录状态)
    token: localStorage.getItem('token') || null,
    // 从本地存储获取角色列表(刷新后仍能保持权限信息)
    roles: localStorage.getItem('roles') ? JSON.parse(localStorage.getItem('roles')) : [],
  }),
  actions: {
    /**
     * 登录方法:存储用户信息并持久化到本地
     * @param {string} role - 用户选择的角色(如 'admin' 或 'user')
     */
    login(role) {
      this.token = 'fake-token-' + role // 生成模拟token
      this.roles = [role] // 存储用户角色
      localStorage.setItem('token', this.token) // 持久化token
      localStorage.setItem('roles', JSON.stringify(this.roles)) // 持久化角色
      console.log('登录成功,角色:', this.roles)
    },
    /**
     * 退出登录方法:清除用户信息
     */
    logout() {
      this.token = null // 清空token
      this.roles = [] // 清空角色
      localStorage.removeItem('token') // 移除本地token
      localStorage.removeItem('roles') // 移除本地角色
    },
  },
})

// 应用状态存储(useAppStore)
export const useAppStore = defineStore('app', {
  state: () => ({
    hasRoutes: false, // 标记动态路由是否已加载
  }),
  actions: {
    /**
     * 更新路由加载状态
     * @param {boolean} status - 路由加载状态(true=已加载,false=未加载)
     */
    setHasRoutes(status) {
      this.hasRoutes = status
    },
  },
})

核心逻辑

  • useUserStore:存储用户的 token 和 roles,并通过 localStorage 持久化,确保刷新后状态不丢失。
  • login 方法:登录时保存用户角色并持久化,实现 “记住登录状态”。
  • logout 方法:退出时清除所有用户信息,确保安全退出。
  • useAppStore:通过 hasRoutes 标记动态路由是否已加载,避免重复加载。

3. 权限过滤工具:筛选用户可访问的路由

这部分是权限控制的核心,通过两个函数实现 “根据用户角色筛选路由” 的功能。

// src/utils/route.js
/**
 * 根据用户角色筛选动态路由(核心权限过滤函数)
 * @param {Array} routes - 待筛选的动态路由列表(asyncRoutes)
 * @param {Array} roles - 用户角色列表(如 ['admin'] 或 ['user'])
 * @returns {Array} 筛选后用户可访问的路由列表
 */
export function filterAsyncRoutes(routes, roles) {
  const validRoutes = [] // 存储符合条件的路由
  
  // 遍历所有动态路由
  routes.forEach((route) => {
    const tempRoute = { ...route } // 复制路由对象(避免修改原数组)
    
    // 检查用户是否有权访问当前路由
    if (hasPermission(roles, tempRoute)) {
      // 如果路由有子路由,递归筛选子路由(支持多级路由)
      if (tempRoute.children && tempRoute.children.length) {
        tempRoute.children = filterAsyncRoutes(tempRoute.children, roles)
      }
      // 将有权访问的路由添加到结果数组
      validRoutes.push(tempRoute)
    }
  })
  
  console.log('筛选后的路由:', validRoutes) // 调试日志
  return validRoutes // 返回筛选结果
}

/**
 * 检查用户是否有权访问单个路由(辅助函数)
 * @param {Array} roles - 用户角色列表
 * @param {Object} route - 单个路由配置对象
 * @returns {boolean} true=有权访问,false=无权访问
 */
export function hasPermission(roles, route) {
  // 如果路由配置了 meta.roles,则需要检查权限
  if (route.meta && route.meta.roles) {
    // 检查用户角色中是否有任意一个角色在路由允许的角色列表中
    // some() 方法:只要有一个元素满足条件就返回 true
    return roles.some((role) => route.meta.roles.includes(role))
  } else {
    // 路由未配置 meta.roles,默认允许所有用户访问
    return true
  }
}

核心逻辑

  • filterAsyncRoutes:递归遍历动态路由,通过 hasPermission 检查每个路由的访问权限,最终返回用户可访问的路由列表。
  • hasPermission:判断单个路由是否允许用户访问,核心逻辑是 “用户角色与路由要求的角色是否有交集”。
  • 支持多级路由:通过递归处理 children 子路由,确保子路由也受权限控制。

4. 全局路由守卫:控制路由加载与跳转逻辑

路由守卫是动态路由加载的 “触发器”,在每次路由跳转前检查权限并加载动态路由。

// src/permission.js
import router from './router'
import { useUserStore, useAppStore } from './store'
import { filterAsyncRoutes } from './utils/route'
import { asyncRoutes } from './router'

/**
 * 全局前置守卫:在路由跳转前执行
 * 作用:检查登录状态、加载动态路由、控制页面访问权限
 */
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore() // 获取用户状态
  const appStore = useAppStore() // 获取应用状态
  const hasToken = userStore.token // 检查用户是否已登录

  if (hasToken) { // 已登录状态
    if (to.path === '/login') { 
      // 已登录用户访问登录页时,重定向到首页
      next('/')
    } else { 
      // 访问非登录页时,检查动态路由是否已加载
      if (!appStore.hasRoutes) { 
        // 动态路由未加载:执行加载逻辑
        try {
          const roles = userStore.roles || [] // 获取用户角色
          // 筛选用户可访问的动态路由
          const a***essRoutes = filterAsyncRoutes(asyncRoutes, roles)
          
          // 动态添加路由到路由实例
          a***essRoutes.forEach((route) => {
            // 避免重复添加路由
            if (!router.hasRoute(route.name)) {
              router.addRoute(route)
            }
          })
          
          // 添加404路由(放在最后,确保所有路由都匹配失败后才触发)
          if (!router.hasRoute('404')) {
            router.addRoute({
              path: '/:pathMatch(.*)*', // 匹配所有未定义的路由
              redirect: '/404', // 重定向到404页
              hidden: true,
              name: '404',
            })
          }
          
          // 标记动态路由已加载(避免重复加载)
          appStore.setHasRoutes(true)
          // 重新导航到目标路由(确保动态路由生效)
          next(to.path)
        } catch (error) {
          // 加载失败:清除登录状态并跳转到登录页
          console.error('加载动态路由失败:', error)
          userStore.logout()
          next('/login')
        }
      } else { 
        // 动态路由已加载:直接放行
        next()
      }
    }
  } else { // 未登录状态
    if (to.path === '/login') { 
      // 未登录用户访问登录页:直接放行
      next()
    } else { 
      // 未登录用户访问其他页:重定向到登录页
      next('/login')
    }
  }
})

核心逻辑

  • 区分 “已登录” 和 “未登录” 状态,未登录用户只能访问登录页。
  • 已登录用户首次访问或刷新页面时,appStore.hasRoutes 为 false,触发动态路由加载。
  • 动态路由加载流程:筛选路由 → 添加到路由实例 → 标记加载状态 → 重新导航。
  • 404 路由最后添加:确保所有有效路由都匹配失败后才跳转 404 页。

5. 侧边栏组件:动态渲染菜单,解决刷新路由丢失问题

侧边栏组件负责根据用户权限动态渲染可访问的菜单,核心是 “实时响应路由变化”。

<template>
  <el-container style="min-height: 100vh">
    <!-- 侧边栏:仅当用户登录(有token)时显示 -->
    <el-aside width="200px" v-if="userStore.token">
      <el-menu 
        default-active="Dashboard" 
        class="el-menu-vertical-demo" 
        @select="handleMenuSelect"
      >
        <!-- 遍历去重后的路由列表生成菜单 -->
        <template v-for="route in uniqueRoutes" :key="route.path">
          <el-menu-item 
            v-if="!route.hidden && route.name" 
            :index="route.name"
          >
            {{ route.meta.title || route.name }}
          </el-menu-item>
        </template>
      </el-menu>
    </el-aside>
    <el-main>
      <router-view /> <!-- 显示当前路由对应的页面 -->
    </el-main>
  </el-container>
</template>

<script setup>
import { useRouter } from 'vue-router'
import { useUserStore } from './store'
import { ***puted, onMounted, ref } from 'vue'
import { constantRoutes } from './router'

const router = useRouter()
const userStore = useUserStore()
// 用于强制刷新的响应式变量(解决路由变化不触发更新问题)
const refreshKey = ref(0)

/**
 * 检查用户是否有权访问单个路由(与 utils/route.js 逻辑一致)
 * @param {Object} route - 路由配置对象
 * @returns {boolean} 权限检查结果
 */
const hasPermission = (route) => {
  const roles = userStore.roles || []
  if (route.meta && route.meta.roles) {
    return roles.some(role => route.meta.roles.includes(role))
  }
  return true
}

/**
 * 过滤出符合条件的路由(计算属性)
 * 作用:合并固定路由和动态路由,只保留需要显示的路由
 */
const filteredRoutes = ***puted(() => {
  // 引用refreshKey,当它变化时强制重新计算
  refreshKey.value 
  
  // 合并固定路由和当前所有已加载的路由
  const allRoutes = [...constantRoutes, ...router.getRoutes()]
  
  // 过滤条件:不隐藏 + 有标题 + 有权限
  return allRoutes.filter(route => {
    return !route.hidden && route.meta && route.meta.title && hasPermission(route)
  })
})

/**
 * 去重后的路由列表(计算属性)
 * 作用:避免路由重复(合并时可能出现重复路由)
 */
const uniqueRoutes = ***puted(() => {
  const routesMap = {}
  filteredRoutes.value.forEach(route => {
    routesMap[route.name] = route // 用name去重
  })
  console.log('当前侧边栏显示的路由:', Object.values(routesMap))
  return Object.values(routesMap)
})

/**
 * 页面挂载后执行:监听路由变化,确保侧边栏实时更新
 */
onMounted(() => {
  // 路由跳转完成后触发(替代不可靠的watch监听)
  router.afterEach(() => {
    console.log('路由跳转完成,更新侧边栏')
    refreshKey.value++ // 改变refreshKey,强制路由列表重新计算
  })
})

/**
 * 处理菜单点击事件
 * @param {string} index - 路由名称(对应路由的name属性)
 */
const handleMenuSelect = (index) => {
  router.push({ name: index }) // 跳转到对应的路由
}
</script>

<style>
.el-menu-vertical-demo:not(.el-menu--collapse) {
  width: 200px;
  min-height: 400px;
}
</style>

核心逻辑

  • filteredRoutes:合并固定路由和动态路由,过滤出 “不隐藏、有标题、有权限” 的路由。
  • uniqueRoutes:通过路由 name 去重,避免菜单重复显示。
  • router.afterEach:监听路由跳转完成事件,通过 refreshKey 强制更新菜单,解决 “动态路由添加后菜单不更新” 的问题。
  • 响应式更新:通过计算属性和 refreshKey 确保路由变化后菜单实时刷新。

转载请说明出处内容投诉
CSS教程网 » Vue 动态路由权限控制:解决刷新路由丢失问题

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买