66. 路由元信息与权限控制

📖 概述

路由元信息是Vue Router中用于存储路由附加信息的机制,而权限控制则是根据用户权限决定是否允许访问特定路由的功能。结合路由元信息和导航守卫,我们可以实现灵活、高效的权限控制方案。本集将深入讲解Vue Router 4.x中路由元信息的定义、扩展、使用,以及如何基于路由元信息实现权限控制,帮助你构建更安全、更灵活的单页应用。

✨ 核心知识点

1. 路由元信息基础

什么是路由元信息

  • 路由元信息是在路由配置中使用meta字段存储的附加信息
  • 可以包含任意自定义数据,如标题、图标、权限要求等
  • 元信息会被传递到路由对象中,可以在组件和导航守卫中访问
  • 适用于权限控制、菜单生成、页面标题设置等场景

配置路由元信息

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

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'home',
    component: () => import('../views/HomeView.vue'),
    meta: {
      title: '首页', // 页面标题
      icon: 'home', // 菜单图标
      requiresAuth: false, // 是否需要认证
      showInMenu: true // 是否在菜单中显示
    }
  },
  {
    path: '/dashboard',
    name: 'dashboard',
    component: () => import('../views/DashboardView.vue'),
    meta: {
      title: '仪表盘',
      icon: 'dashboard',
      requiresAuth: true,
      showInMenu: true,
      permissions: ['dashboard:view'] // 权限要求
    }
  },
  {
    path: '/admin',
    name: 'admin',
    component: () => import('../views/AdminLayout.vue'),
    meta: {
      title: '管理中心',
      icon: 'settings',
      requiresAuth: true,
      showInMenu: true,
      permissions: ['admin:view']
    },
    children: [
      {
        path: 'users',
        name: 'admin-users',
        component: () => import('../views/AdminUsersView.vue'),
        meta: {
          title: '用户管理',
          icon: 'users',
          requiresAuth: true,
          showInMenu: true,
          permissions: ['user:manage']
        }
      },
      {
        path: 'roles',
        name: 'admin-roles',
        component: () => import('../views/AdminRolesView.vue'),
        meta: {
          title: '角色管理',
          icon: 'lock',
          requiresAuth: true,
          showInMenu: true,
          permissions: ['role:manage']
        }
      }
    ]
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

export default router

2. 扩展路由元信息类型

使用TypeScript扩展路由元信息

为了获得更好的TypeScript类型支持,我们可以扩展Vue Router的RouteMeta接口。

// src/router/types.ts
import 'vue-router'

// 扩展路由元信息类型
declare module 'vue-router' {
  interface RouteMeta {
    // 页面标题
    title?: string
    // 菜单图标
    icon?: string
    // 是否需要认证
    requiresAuth?: boolean
    // 是否在菜单中显示
    showInMenu?: boolean
    // 权限要求
    permissions?: string[]
    // 页面缓存
    keepAlive?: boolean
    // 面包屑导航
    breadcrumb?: boolean
    // 页面过渡动画
    transition?: string
  }
}

3. 访问路由元信息

在组件中访问元信息

1. 使用Composition API
<script setup lang="ts">
import { useRoute } from 'vue-router'

const route = useRoute()

// 访问路由元信息
const pageTitle = route.meta.title
const requiresAuth = route.meta.requiresAuth
const permissions = route.meta.permissions
</script>
2. 使用Options API
<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  computed: {
    pageTitle() {
      return this.$route.meta.title
    },
    requiresAuth() {
      return this.$route.meta.requiresAuth
    }
  }
})
</script>

在导航守卫中访问元信息

// src/router/index.ts
router.beforeEach((to, from, next) => {
  // 访问路由元信息
  const requiresAuth = to.meta.requiresAuth || false
  const permissions = to.meta.permissions || []
  
  // 权限控制逻辑
  if (requiresAuth && !isAuthenticated()) {
    next({ name: 'login' })
    return
  }
  
  if (permissions.length > 0 && !hasPermissions(permissions)) {
    next({ name: '403' })
    return
  }
  
  next()
})

4. 基于路由元信息的权限控制

1. 实现权限控制逻辑

// src/utils/permission.ts

// 模拟用户权限
const userPermissions: string[] = ['dashboard:view', 'user:manage']

// 检查用户是否已认证
export function isAuthenticated(): boolean {
  return !!localStorage.getItem('token')
}

// 检查用户是否有权限
export function hasPermission(permission: string): boolean {
  return userPermissions.includes(permission)
}

// 检查用户是否有多个权限
export function hasPermissions(permissions: string[]): boolean {
  return permissions.every(permission => hasPermission(permission))
}

// 检查用户是否有任一权限
export function hasAnyPermission(permissions: string[]): boolean {
  return permissions.some(permission => hasPermission(permission))
}

2. 在导航守卫中实现权限控制

// src/router/index.ts
import { isAuthenticated, hasPermissions } from '../utils/permission'

router.beforeEach((to, from, next) => {
  // 设置页面标题
  if (to.meta.title) {
    document.title = `${to.meta.title} - Vue Admin`
  }
  
  // 权限控制
  const requiresAuth = to.meta.requiresAuth || false
  const permissions = to.meta.permissions || []
  
  // 检查是否需要认证
  if (requiresAuth && !isAuthenticated()) {
    next({ 
      name: 'login',
      query: { redirect: to.fullPath } // 保存重定向路径
    })
    return
  }
  
  // 检查权限
  if (permissions.length > 0 && !hasPermissions(permissions)) {
    next({ name: '403' })
    return
  }
  
  next()
})

3. 动态生成菜单

// src/utils/menu.ts
import { RouteRecordRaw } from 'vue-router'
import { hasPermissions } from './permission'

// 根据路由配置和用户权限生成菜单
export function generateMenu(routes: Array<RouteRecordRaw>): any[] {
  const menu: any[] = []
  
  routes.forEach(route => {
    // 跳过不在菜单中显示的路由
    if (!route.meta?.showInMenu) {
      return
    }
    
    // 检查权限
    if (route.meta?.permissions && !hasPermissions(route.meta.permissions)) {
      return
    }
    
    const menuItem: any = {
      path: route.path,
      name: route.name,
      title: route.meta?.title,
      icon: route.meta?.icon,
      children: []
    }
    
    // 处理子路由
    if (route.children && route.children.length > 0) {
      menuItem.children = generateMenu(route.children)
      // 如果没有子菜单,跳过父菜单
      if (menuItem.children.length === 0) {
        return
      }
    }
    
    menu.push(menuItem)
  })
  
  return menu
}

5. 路由元信息的高级应用

1. 页面缓存控制

// 配置路由元信息控制页面缓存
const routes: Array<RouteRecordRaw> = [
  {
    path: '/dashboard',
    name: 'dashboard',
    component: () => import('../views/DashboardView.vue'),
    meta: {
      keepAlive: true // 启用页面缓存
    }
  },
  {
    path: '/user/:id',
    name: 'user',
    component: () => import('../views/UserView.vue'),
    meta: {
      keepAlive: false // 禁用页面缓存
    }
  }
]
<!-- App.vue -->
<template>
  <div>
    <!-- 根据meta.keepAlive控制是否缓存页面 -->
    <router-view v-slot="{ Component }">
      <keep-alive>
        <component 
          :is="Component" 
          v-if="$route.meta.keepAlive"
        />
      </keep-alive>
      <component 
        :is="Component" 
        v-if="!$route.meta.keepAlive"
      />
    </router-view>
  </div>
</template>

2. 页面过渡动画

// 配置路由元信息控制页面过渡动画
const routes: Array<RouteRecordRaw> = [
  {
    path: '/home',
    name: 'home',
    component: () => import('../views/HomeView.vue'),
    meta: {
      transition: 'fade' // 淡入淡出动画
    }
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('../views/AboutView.vue'),
    meta: {
      transition: 'slide' // 滑动动画
    }
  }
]
<!-- App.vue -->
<template>
  <div>
    <!-- 根据meta.transition设置过渡动画 -->
    <router-view v-slot="{ Component }">
      <transition :name="$route.meta.transition || 'default'">
        <component :is="Component" />
      </transition>
    </router-view>
  </div>
</template>

<style>
/* 淡入淡出动画 */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* 滑动动画 */
.slide-enter-active,
.slide-leave-active {
  transition: transform 0.3s ease;
}

.slide-enter-from {
  transform: translateX(100%);
}

.slide-leave-to {
  transform: translateX(-100%);
}

/* 默认动画 */
.default-enter-active,
.default-leave-active {
  transition: all 0.3s ease;
}

.default-enter-from,
.default-leave-to {
  opacity: 0;
  transform: translateY(20px);
}
</style>

3. 面包屑导航生成

// src/utils/breadcrumb.ts
import { RouteRecordRaw } from 'vue-router'

// 根据当前路由生成面包屑
export function generateBreadcrumb(routes: Array<RouteRecordRaw>, currentPath: string): any[] {
  const breadcrumb: any[] = []
  const pathSegments = currentPath.split('/').filter(segment => segment)
  
  let currentPathSegment = ''
  
  pathSegments.forEach(segment => {
    currentPathSegment += `/${segment}`
    
    // 查找对应的路由
    const route = findRouteByPath(routes, currentPathSegment)
    if (route && route.meta?.breadcrumb !== false) {
      breadcrumb.push({
        path: currentPathSegment,
        name: route.name,
        title: route.meta?.title || segment
      })
    }
  })
  
  return breadcrumb
}

// 递归查找路由
function findRouteByPath(routes: Array<RouteRecordRaw>, path: string): RouteRecordRaw | undefined {
  for (const route of routes) {
    if (route.path === path) {
      return route
    }
    
    if (route.children) {
      const found = findRouteByPath(route.children, path)
      if (found) {
        return found
      }
    }
  }
  
  return undefined
}

6. 动态权限控制

1. 动态添加路由

// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { hasPermissions } from '../utils/permission'

// 基础路由
const baseRoutes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'home',
    component: () => import('../views/HomeView.vue')
  },
  {
    path: '/login',
    name: 'login',
    component: () => import('../views/LoginView.vue')
  }
]

// 需要权限的路由
const protectedRoutes: Array<RouteRecordRaw> = [
  {
    path: '/admin',
    name: 'admin',
    component: () => import('../views/AdminLayout.vue'),
    meta: {
      requiresAuth: true,
      permissions: ['admin:view']
    },
    children: [
      // 子路由...
    ]
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: baseRoutes
})

// 动态添加权限路由
export function addProtectedRoutes() {
  protectedRoutes.forEach(route => {
    // 检查路由权限
    if (!route.meta?.permissions || hasPermissions(route.meta.permissions)) {
      router.addRoute(route)
      
      // 递归添加子路由
      if (route.children) {
        addChildRoutes(route.name as string, route.children)
      }
    }
  })
}

// 递归添加子路由
function addChildRoutes(parentName: string, children: Array<RouteRecordRaw>) {
  children.forEach(child => {
    // 检查子路由权限
    if (!child.meta?.permissions || hasPermissions(child.meta.permissions)) {
      router.addRoute(parentName, child)
      
      // 递归添加嵌套子路由
      if (child.children) {
        addChildRoutes(child.name as string, child.children)
      }
    }
  })
}

export default router

2. 在登录后添加路由

<!-- LoginView.vue -->
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { addProtectedRoutes } from '../router'

const router = useRouter()

async function handleLogin() {
  // 登录逻辑
  const token = await login()
  localStorage.setItem('token', token)
  
  // 添加权限路由
  addProtectedRoutes()
  
  // 重定向到首页或之前的页面
  const redirect = router.currentRoute.value.query.redirect as string || '/'
  router.push(redirect)
}
</script>

📝 最佳实践

  1. 合理设计元信息结构

    • 根据项目需求设计元信息字段
    • 使用TypeScript扩展元信息类型,提高类型安全性
  2. 权限控制最佳实践

    • 使用导航守卫实现集中式权限控制
    • 结合路由元信息和用户权限进行检查
    • 实现动态路由加载,只添加用户有权限访问的路由
  3. 菜单生成最佳实践

    • 根据路由元信息动态生成菜单
    • 考虑菜单的嵌套结构
    • 支持菜单图标、标题、权限等配置
  4. 页面标题设置

    • 在导航守卫中统一设置页面标题
    • 支持国际化的页面标题
  5. 页面缓存策略

    • 根据页面特性决定是否缓存
    • 避免过度缓存导致的性能问题
  6. 页面过渡动画

    • 根据页面内容选择合适的过渡动画
    • 保持动画的一致性和流畅性

💡 常见问题与解决方案

  1. 路由元信息不生效

    • 检查路由配置中是否正确添加了meta字段
    • 确保TypeScript类型扩展已正确配置
    • 检查是否在组件或导航守卫中正确访问元信息
  2. 权限控制不生效

    • 检查导航守卫中的权限检查逻辑
    • 确保用户权限已正确获取和存储
    • 检查路由元信息中的权限配置
  3. 动态路由添加失败

    • 确保在登录后调用添加路由的函数
    • 检查路由名称是否唯一
    • 确保父路由已正确添加
  4. 菜单生成错误

    • 检查菜单生成函数的逻辑
    • 确保路由配置的嵌套结构正确
    • 检查权限检查逻辑

📚 进一步学习资源

🎯 课后练习

  1. 基础练习

    • 配置路由元信息
    • 扩展路由元信息类型
    • 在组件和导航守卫中访问元信息
  2. 进阶练习

    • 基于路由元信息实现权限控制
    • 动态生成菜单
    • 实现页面缓存控制
  3. 实战练习

    • 构建一个完整的权限控制系统
    • 实现动态路由加载
    • 生成带有权限控制的菜单
  4. 性能优化练习

    • 优化动态路由添加逻辑
    • 优化菜单生成性能
    • 优化页面缓存策略

通过本集的学习,你已经掌握了Vue Router 4.x中路由元信息的定义、扩展、使用,以及基于路由元信息的权限控制、菜单生成、页面缓存控制等高级应用。在实际项目中,合理运用路由元信息,能够构建出更安全、更灵活、更易于维护的单页应用。下一集我们将深入学习路由守卫深度解析,进一步提升Vue 3路由系统的使用能力。

« 上一篇 Vue3 + TypeScript 系列教程 - 第65集:命名路由与命名视图 下一篇 » Vue3 + TypeScript 系列教程 - 第67集:路由守卫深度解析