Skip to content

路由相关问题

markdown
用户访问页面 ->
  判断是否登录 ->
    是 -> 是否已经注册动态路由 ->
      否 -> 请求后端菜单 -> 转成 routes -> addRoute -> next({ ...to, replace: true })
      是 -> 正常放行
    否 -> 判断是否在白名单 ->
      是 -> 放行
      否 -> 重定向登录页

Vue 3 + Vue Router 4 + Pinia 动态路由权限控制方案文档

📘 一、项目核心依赖

  • Vue 3
  • Vue Router 4
  • Pinia
  • Axios
  • TypeScript(推荐)

📁 二、项目目录结构

bash
src/
├── router/
   ├── index.ts                  # 初始化静态路由
   ├── dynamicRoutes.ts         # Layout 嵌套下的动态子路由
   ├── permission.ts            # 路由守卫 + 权限控制核心逻辑
├── stores/
   ├── routesList.ts            # 存储动态路由 & 菜单
   └── userInfo.ts              # 用户信息、角色权限
├── views/
   └── xxx.vue                  # 页面组件
├── layout/
   └── index.vue                # Layout 主框架
└── utils/
    ├── auth.ts                  # token 存取
    └── menuToRoute.ts           # 后端菜单转成 RouteRecordRaw

🔁 三、权限流程图

text
初始化 Vue App ->
  静态路由注册完成 ->
    路由守卫开始 ->
      如果未登录 -> 重定向登录页
      如果已登录且未加载动态路由 ->
        拉取菜单接口 -> 转换为 RouteRecordRaw -> addRoute ->
        next({ ...to, replace: true }) 强制跳转已注册的路由
      如果已加载过动态路由 -> next()

🧩 四、静态路由配置(router/index.ts

typescript
const staticRoutes: RouteRecordRaw[] = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/login.vue')
  },
  {
    path: '/:path(.*)*',
    name: 'notFound',
    component: () => import('@/views/error/404.vue')
  }
];

🧱 五、动态路由模板(dynamicRoutes.ts

typescript
export const dynamicRoutes: RouteRecordRaw[] = [
  {
    path: '/',
    name: '/',
    component: () => import('@/layout/index.vue'),
    redirect: '/home',
    children: [] // 后端菜单处理后放这里
  }
];

⚙️ 六、菜单转换工具(utils/menuToRoute.ts

typescript
export function transformMenusToRoutes(menus: any[]): RouteRecordRaw[] {
  return menus.map(menu => ({
    path: menu.path,
    name: menu.name,
    component: () => import(`@/views/${menu.component}.vue`),
    meta: menu.meta
  }));
}

🔒 七、权限守卫逻辑(router/permission.ts

typescript
router.beforeEach(async (to, from, next) => {
  const token = getToken();
  const whiteList = ['/login'];

  if (!token && !whiteList.includes(to.path)) {
    return next('/login');
  }

  const storesRoutesList = useRoutesList(pinia);
  const { routesList } = storeToRefs(storesRoutesList);

  if (token && routesList.value.length === 0) {
    const menus = await getMenuFromServer();
    const dynamicChildren = transformMenusToRoutes(menus);

    dynamicRoutes[0].children = dynamicChildren;
    router.addRoute(dynamicRoutes[0]);

    routesList.value = dynamicChildren;

    return next({ ...to, replace: true });
  }

  next();
});

✅ 八、注意事项清单

建议
addRoute() 位置在路由守卫中首次登录后添加
next() 跳转方式使用 `next({ ...to, replace: true })` 避免匹配失败
404/401 路由也放到 layout.children 下
菜单转换使用工具方法处理 component 路径映射
防止重复注册用 Pinia 或标志变量控制只注册一次
动态 children 添加只注册 layout 层,children 自动注册

🎁 九、进阶拓展

  • 动态根据角色过滤菜单
  • 添加按钮级别权限指令(如 v-permission
  • 动态侧边栏高亮激活处理
  • 页面缓存(配合 meta.isKeepAlive

📦 十、后端返回菜单结构示例

json
[
  {
    "path": "/home",
    "name": "Home",
    "component": "home/index",
    "meta": {
      "title": "首页",
      "roles": ["admin", "common"]
    }
  },
  {
    "path": "/user",
    "name": "User",
    "component": "user/index",
    "meta": {
      "title": "用户管理",
      "roles": ["admin"]
    }
  }
]

基于 Obsidian + VitePress 构建