vue3+router动态权限路由

开飞机的舒克 halo.gaodaimou.cn

动态路由实现分析

1. 概述

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

2. 核心文件结构

文件路径 主要功能
src/router/index.ts 路由配置、动态路由生成、路由守卫
src/stores/user.ts 用户状态管理、权限数据存储
src/views/ 所有视图组件目录

3. 数据结构定义

3.1 权限数据结构

typescript 复制代码
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 用户状态结构

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

4. 动态路由生成机制

4.1 组件预加载

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

ini 复制代码
const modules = import.meta.glob("@/views/**/*.vue");

4.2 路由生成函数

javascript 复制代码
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 核心逻辑

php 复制代码
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 状态初始化

javascript 复制代码
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 权限数据更新

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

6.3 认证清除

ini 复制代码
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 组件路径校验增强

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

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

8.2 路由缓存机制

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

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

8.3 权限粒度细化

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

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

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

8.4 路由生成错误处理增强

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

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

8.5 路由懒加载优化

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

xml 复制代码
<!-- 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. 参考文献

相关推荐
IT_陈寒7 分钟前
Redis大key删除为什么会导致服务卡顿?我的线上事故复盘
前端·人工智能·后端
用户2705077725241 小时前
MCP 最小 Server 实测:3 个 JSON-RPC 报错与修法
前端
用户499335025071 小时前
一人VIP账号多人共用:Web 代理里的会话共享怎么实现
前端
用户老李1 小时前
无线电自学教程 05:第一台收音机——从矿石机到 SDR
前端
IPIPD1 小时前
Node.js请求遇到503怎么重试?先解析Retry-After,再做有上限退避
前端
葡萄城技术团队1 小时前
dsh-spreadjs-editor 的集成链路:把 SpreadJS Designer 嵌进 Agent 工作台
前端
FlyWIHTSKY2 小时前
vue2和vue3有什么区别,用法上有哪些区别
前端·javascript·vue.js
码云之上2 小时前
10 颗一模一样的「Edit」:中后台 Web Agent 怎么找到对的那一行
前端·人工智能·浏览器
量化分析码农2 小时前
【Python量化多因子实战 04】因子暴露不是 alpha:市值与行业中性化实战
前端·后端
不可能片场3 小时前
Electron 发布 正文写入的三条通道与读回口径
前端·electron