网站Logo 开飞机的舒克

vue3+router动态权限路由

dddd
12
2025-12-04

动态路由实现分析

1. 概述

本项目采用了基于权限的动态路由方案,通过后端返回的权限数据,在前端动态生成路由配置并添加到路由实例中。这种方案实现了根据用户权限动态加载可访问的页面,提高了系统的安全性和灵活性。

2. 核心文件结构

文件路径

主要功能

src/router/index.ts

路由配置、动态路由生成、路由守卫

src/stores/user.ts

用户状态管理、权限数据存储

src/views/

所有视图组件目录

3. 数据结构定义

3.1 权限数据结构

interface Permission {
  code: string; // 权限编码
  component: string | null; // 组件路径
  created_at: string; // 创建时间
  icon: string; // 菜单图标
  id: number; // 权限ID
  name: string; // 菜单名称
  parent_id: number; // 父级菜单ID
  path: string; // 路由路径
  sort_order: number; // 排序顺序
  status: number; // 状态(1为启用,0为禁用)
  type: number; // 权限类型
}

3.2 用户状态结构

interface UserState {
  token: string; // 用户认证token
  userInfo: UserInfo; // 用户信息
  permissions: Permission[]; // 权限列表
  isRoutesGenerated: boolean; // 路由是否已生成
}

4. 动态路由生成机制

4.1 组件预加载

使用Vite的import.meta.globAPI预加载所有视图组件,生成组件路径到组件的映射:

const modules = import.meta.glob("@/views/**/*.vue");

4.2 路由生成函数

export function generateRoutes(permissions: Permission[]): RouteRecordRaw[] {
  // 1. 过滤有效的权限数据
  const validPermissions = permissions.filter(
    (p) => p.component && p.status === 1 && p.parent_id !== 0
  );

  // 2. 生成路由配置
  const routes: RouteRecordRaw[] = validPermissions
    .map((permission) => {
      // 查找父级权限
      const parentPermission = permissions.find(
        (p) => p.id === permission.parent_id
      );

      // 转换组件路径并动态导入组件
      const componentPath = permission.component!.replace("@/", "/src/");
      const componentImport = modules[componentPath];

      if (!componentImport) {
        console.warn(`未找到对应的组件: ${permission.component}`);
        return null;
      }

      // 生成路由对象
      return {
        path: `/${permission.path}`,
        name: permission.code.replace(/:/g, ""),
        component: componentImport,
        meta: {
          title: permission.name,
          module: parentPermission?.name || "",
        },
      };
    })
    .filter(Boolean) as RouteRecordRaw[];

  // 3. 构建完整路由配置
  const newRoutes = [
    {
      path: "/",
      name: "main",
      component: () => import("@/views/Home.vue"),
      children: routes, // 将生成的路由作为子路由添加到主路由
    },
  ];
  return newRoutes;
}

5. 路由守卫实现

5.1 核心逻辑

router.beforeEach((to, _from, next) => {
  // 初始化时从本地存储获取token和权限数据
  userStore.initFromStorage();

  const token = userStore.token;
  const hasPermission = userStore.permissions.length > 0;
  const isRoutesGenerated = userStore.isRoutesGenerated;

  // 白名单路由直接放行
  if (whiteList.includes(to.path)) {
    // 已登录用户访问登录页,重定向到首页
    if (token && to.path === "/login") {
      next({ path: "/", replace: true });
    } else {
      next();
    }
    return;
  }

  // 未登录用户访问受限路由,跳转到登录页
  if (!token) {
    next({ path: "/login", replace: true });
    return;
  }

  // 已登录但未生成动态路由且有权限数据
  if (token && hasPermission && !isRoutesGenerated) {
    try {
      // 生成动态路由
      const dynamicRoutes = generateRoutes(userStore.permissions);

      // 添加路由到路由实例
      dynamicRoutes.forEach((route) => {
        router.addRoute(route);
      });

      // 更新路由生成状态
      userStore.setRoutesGenerated(true);

      // 设置默认重定向
      if (dynamicRoutes[0]!.children!.length > 0 && to.path === "/") {
        const firstRoute = dynamicRoutes[0]!.children![0];
        if (firstRoute && firstRoute.path) {
          next({ path: firstRoute.path, replace: true });
          return;
        }
      }

      // 重新导航以应用新路由
      next({ ...to, replace: true });
    } catch (error) {
      console.error("Generate routes error:", error);
      ElMessage.error("获取权限失败,请重新登录");
      userStore.clearAuth();
      next({ path: "/login", replace: true });
    }
    return;
  }

  // 已登录但没有权限数据
  if (token && !hasPermission && !isRoutesGenerated) {
    ElMessage.warning("暂无访问权限");
    next(false); // 阻止导航
    return;
  }

  // 已生成路由,直接放行
  next();
});

5.2 路由守卫流程图

开始导航
  │
  ├─► 检查白名单
  │   ├─► 是白名单路由
  │   │   ├─► 已登录且访问登录页 → 重定向到首页
  │   │   └─► 其他情况 → 放行
  │   │
  │   └─► 非白名单路由
  │       ├─► 未登录 → 重定向到登录页
  │       │
  │       └─► 已登录
  │           ├─► 已生成路由 → 放行
  │           │
  │           └─► 未生成路由
  │               ├─► 没有权限数据 → 提示无权限,阻止导航
  │               │
  │               └─► 有权限数据
  │                   ├─► 生成动态路由
  │                   ├─► 添加路由到路由实例
  │                   ├─► 更新路由生成状态
  │                   ├─► 当前是首页 → 重定向到第一个有权限的页面
  │                   └─► 其他页面 → 重新导航以应用新路由
  │
  └─► 结束

6. 用户状态管理

6.1 状态初始化

initFromStorage() {
  // 从localStorage或sessionStorage获取token
  const localToken = localStorage.getItem("token");
  const sessionToken = sessionStorage.getItem("token");
  this.token = localToken || sessionToken || "";

  // 从localStorage恢复用户信息
  const storedUserInfo = localStorage.getItem("userInfo");
  if (storedUserInfo) {
    try {
      this.userInfo = JSON.parse(storedUserInfo);
    } catch (e) {
      console.error("Failed to parse stored user info", e);
    }
  }

  // 从localStorage恢复权限数据
  const storedPermissions = localStorage.getItem("permissions");
  if (storedPermissions) {
    try {
      this.permissions = JSON.parse(storedPermissions);
    } catch (e) {
      console.error("Failed to parse stored permissions", e);
    }
  }
}

6.2 权限数据更新

setPermissions(permissions: Permission[]) {
  this.permissions = permissions;
  this.isRoutesGenerated = false; // 重置路由生成状态
  // 存储权限数据到localStorage
  localStorage.setItem("permissions", JSON.stringify(permissions));
}

6.3 认证清除

clearAuth() {
  this.token = "";
  this.userInfo = {};
  this.permissions = [];
  this.isRoutesGenerated = false;
  localStorage.removeItem("token");
  localStorage.removeItem("userInfo");
  localStorage.removeItem("permissions");
  sessionStorage.removeItem("token");
}

7. 动态路由工作流程

7.1 用户登录流程

  1. 用户输入用户名和密码登录

  2. 登录成功后,后端返回token和权限数据

  3. 前端调用setAuth存储token,调用setPermissions存储权限数据

  4. 用户访问受限路由,触发路由守卫

  5. 路由守卫检测到已登录但未生成路由,调用generateRoutes生成动态路由

  6. 将生成的路由添加到路由实例中,更新路由生成状态

  7. 完成导航,显示用户可访问的页面

7.2 页面刷新流程

  1. 页面刷新,应用重新初始化

  2. 路由守卫触发,调用initFromStorage从本地存储恢复状态

  3. 如果本地存储有token和权限数据,调用generateRoutes生成动态路由

  4. 添加路由到路由实例,完成导航

8. 代码优化建议

8.1 组件路径校验增强

当前代码中,当组件路径无效时,只会输出警告并跳过该路由。建议添加更严格的校验和错误处理:

if (!componentImport) {
  console.error(`组件路径无效: ${permission.component}`);
  // 可以选择抛出错误或记录到错误监控系统
  return null;
}

8.2 路由缓存机制

对于频繁访问的页面,可以考虑实现路由缓存机制,提高页面切换性能:

// 在路由配置中添加缓存标记
return {
  path: `/${permission.path}`,
  name: permission.code.replace(/:/g, ""),
  component: componentImport,
  meta: {
    title: permission.name,
    module: parentPermission?.name || "",
    keepAlive: true, // 添加缓存标记
  },
};

8.3 权限粒度细化

当前实现只支持页面级权限,建议扩展支持按钮级权限:

// 在Permission接口中添加按钮权限字段
export interface Permission {
  // ... 现有字段
  buttonPermissions?: string[]; // 按钮权限列表
}

// 在组件中使用权限判断
const hasPermission = (permissionCode: string) => {
  return userStore.permissions.some((p) =>
    p.buttonPermissions?.includes(permissionCode)
  );
};

8.4 路由生成错误处理增强

当前代码中,路由生成失败时会清除认证信息并重定向到登录页,建议添加更详细的错误日志和恢复机制:

try {
  // 生成动态路由
  const dynamicRoutes = generateRoutes(userStore.permissions);
  // ...
} catch (error) {
  console.error("Generate routes error:", error);
  // 记录错误到监控系统
  // 可以考虑保留当前认证状态,仅提示错误
  ElMessage.error("路由生成失败,请刷新页面重试");
  next(false);
}

8.5 路由懒加载优化

结合Vue 3的Suspense组件,优化路由切换体验:

<!-- App.vue -->
<template>
  <Suspense>
    <template #default>
      <router-view />
    </template>
    <template #fallback>
      <div class="loading">加载中...</div>
    </template>
  </Suspense>
</template>

9. 总结

本项目的动态路由实现具有以下特点:

  1. 基于权限的动态生成:根据后端返回的权限数据,动态生成用户可访问的路由

  2. 组件按需加载:使用Vite的import.meta.glob实现组件的动态导入

  3. 状态持久化:将token、用户信息和权限数据存储到本地,页面刷新后自动恢复

  4. 完善的路由守卫:处理登录验证、动态路由生成和导航控制

  5. 灵活的权限管理:支持多级菜单和权限控制

这种动态路由方案提高了系统的安全性和灵活性,便于后端统一管理用户权限和菜单配置,同时减轻了前端的维护成本。

10. 参考文献