Skyroc Admin React
路由

路由概览

理解 apps/admin 的 TanStack Router 约定式路由、routeTree.gen.ts、路由组和动态菜单模式

这一页解决一个问题:在 apps/admin 中,一个页面文件如何变成可访问的后台路由,并最终进入布局、菜单、标签页和权限链路。

当前实现以 TanStack Router 的 file-based routing 为核心。页面文件放在 apps/admin/src/pages,Vite 插件生成 apps/admin/src/features/router/routeTree.gen.ts,应用启动时再把 route tree 交给 router 和后台布局系统。

适用场景

场景重点位置
新增后台业务页面apps/admin/src/pages/(admin)createFileRoutestaticData
给表单页或编辑页开启页面缓存staticData.keepAlivestaticData.tab.multiAdminContent
新增登录或认证页面apps/admin/src/pages/(auth)、登录 layout route
新增错误页apps/admin/src/pages/(errors)pages/not-found.tsxpages/error.tsx
理解路由文件为什么会变成某个 URL@skyroc/web-admin-vite 的 TanStack Router 插件配置、routeTree.gen.ts
排查菜单有但页面打不开routeTree.gen.ts、动态菜单接口返回的 path、后台真实页面文件
排查登录后跳转、403 或外链打开pages/__root.tsxpages/(admin)/layout.tsxfeatures/router/guard.ts

当前实现位置

文件职责
apps/admin/src/pagesTanStack Router 约定式路由目录,承载 root、layout、index、动态路由和业务页面。
packages/web/admin-vite/src/plugins/router.ts配置 TanStack Router Vite 插件:路由目录、生成文件、layout token、忽略目录等。
apps/admin/src/features/router/routeTree.gen.tsTanStack Router 自动生成的 route tree。不要手改这个文件。
apps/admin/src/features/router/index.ts创建 router 实例,配置 route tree、search 解析、预加载、滚动恢复和初始 context。
apps/admin/src/features/router/RouterProvider.tsxuseAuth() 读取认证和菜单上下文,注入 TanStack Router Provider。
apps/admin/src/pages/__root.tsx根路由。处理全局 loading、error、notFound 和已登录用户的认证预热。
apps/admin/src/pages/(admin)/layout.tsx后台 layout route。渲染 WebAdminLayout,并在进入后台页面前执行 guardAdminRoute()
apps/admin/src/bootstrap.tsxrouteTree、菜单分类、动态路由加载器和权限超级角色交给 setupAdminLayouts()

路由生成链路

当前路由链路可以拆成“构建期生成”和“运行期消费”两段:

apps/admin/src/pages
  -> @tanstack/router-plugin/vite
  -> apps/admin/src/features/router/routeTree.gen.ts
  -> createRouter({ routeTree })
  -> RouterProvider 注入 auth context
  -> root beforeLoad 做已登录认证预热
  -> admin layout beforeLoad 做后台进入守卫
  -> WebAdminLayout、菜单、tabs、权限守卫消费路由信息

构建期:从文件生成 route tree

@skyroc/web-admin-vite 在 Vite 插件中配置 TanStack Router:

tanstackRouter({
  autoCodeSplitting: true,
  generatedRouteTree: './src/features/router/routeTree.gen.ts',
  routeFileIgnorePattern: '(?:^|/)(components|modules)(?:/|$)|(?:^|/)(loading|error|not-found)(?:.tsx?|$)',
  routesDirectory: './src/pages',
  routeToken: 'layout',
  target: 'react'
});

这几个配置决定了当前路由约定:

配置当前含义
routesDirectory只有 apps/admin/src/pages 下面的路由文件会参与生成。
generatedRouteTree生成文件固定在 src/features/router/routeTree.gen.ts,方便 features/router/index.tsbootstrap.tsx 同时引用。
routeToken: 'layout'layout.tsx 表示 layout route,例如 pages/(admin)/layout.tsx
routeFileIgnorePatterncomponentsmodules 目录,以及 loadingerrornot-found 文件不会被当作路由页面。
autoCodeSplitting路由文件会按 TanStack Router 规则自动拆分加载。

因此,页面级组件可以放在页面目录下的 modules,不会意外变成 URL。

运行期:router 和后台布局同时消费 route tree

features/router/index.ts 使用同一个 routeTree 创建 TanStack Router:

export const router = createRouter({
  routeTree,
  defaultPreload: 'intent',
  stringifySearch: stringifyQuery,
  parseSearch: parseQuery,
  scrollRestoration: true
});

bootstrap.tsx 又把同一个 routeTree 传给 setupAdminLayouts()

setupAdminLayouts({
  defaultHome: globalConfig.defaultHome,
  loadDynamicRoutes: loadAdminDynamicRoutes,
  menuCategories,
  permissionSuperRole: import.meta.env.VITE_STATIC_SUPER_ROLE,
  routeMode: globalConfig.routeMode,
  routeTree,
  storage: localStg
});

这个设计有两个结果:

消费方使用 route tree 做什么
TanStack Router匹配 URL、执行 beforeLoad、渲染页面组件、处理 pending/error/notFound。
@skyroc/web-admin-layouts在静态模式下从路由 staticData 生成菜单;在动态模式下仍用 route tree 识别布局分组和真实页面边界。

动态路由模式不是“后端临时注册 React 页面组件”。后台返回的菜单 path 仍然必须能落到前端已经存在的 TanStack Route,否则会在适配阶段被过滤,不进入菜单和动态授权索引。

pages 目录约定

当前 apps/admin/src/pages 主要分为几类:

路径URL 表现职责
__root.tsx根路由,不直接产生业务 URL全局 root route,挂载 loading、error、notFound、NProgress,并只在已有登录态时做认证预热。
index.tsx/根路径重定向:已登录去首页,未登录去 /login
(admin)/layout.tsx不直接出现在 URL 中后台 layout route,承载登录后的主应用壳和后台守卫。
(admin)/home/index.tsx/home后台首页。
(admin)/manage/layout.tsx/manage管理模块 layout route,承载子路由出口和菜单分组。
(admin)/manage/user/index.tsx/manage/user用户管理列表页。
(admin)/manage/user/$id.tsx/manage/user/:id动态参数详情页。
(auth)/login/layout.tsx/login登录页 layout route,已登录时回到 redirect 或首页。
(errors)/403.tsx/403顶层 403 页面。

(admin)(auth)(errors) 是路由组。它们用于组织目录和 layout 关系,不会出现在 URL 中。

路由组和 layout route

(admin) 是后台壳边界

所有登录后访问的后台页面都挂在 pages/(admin) 下。pages/(admin)/layout.tsx 做两件事:

职责当前实现
渲染后台布局使用 @skyroc/web-admin-layoutsAdminLayout,注入 logo、footer、通知按钮、用户头像等 slot。
进入前守卫beforeLoad 调用 guardAdminRoute({ context, location, matches, preload })

这意味着新增后台业务页面时,不需要在每个页面重复写登录校验。只要页面放在 (admin) 下面,就会经过后台 layout route 的守卫。

(auth) 是认证页面边界

登录、注册、验证码登录、重置密码和退出登录放在 (auth) 下。pages/(auth)/login/layout.tsx 不负责认证初始化,只在用户已登录时做跳转兜底:

if (context.isLoggedIn) {
  throw redirect({ to: search.redirect || context.getHomeRoute() });
}

因此,访问 /login 时如果已有有效 token,会回到 redirect 指定路径或当前用户首页。

(errors) 是顶层错误页

pages/(errors)/403.tsx404.tsx500.tsx 生成顶层 /403/404/500。后台内部也有 (admin)/exception/* 页面,用于菜单中的异常页演示。两者职责不同:

类型例子用途
顶层错误页/403/404/500守卫和全局错误跳转使用。
后台异常演示页/exception/403/exception/404/exception/500后台菜单中的业务示例页面。

新增后台页面

新增普通后台页面时,优先放在 apps/admin/src/pages/(admin) 下,并在路由对象上提供 staticData。最小形态如下:

import { createFileRoute } from '@tanstack/react-router';

const ReportCenter = () => {
  return <div>Report Center</div>;
};

export const Route = createFileRoute('/(admin)/report/center')({
  component: ReportCenter,
  staticData: {
    title: 'report_center',
    i18nKey: 'route.report_center',
    menu: {
      icon: 'mdi:file-chart-outline',
      order: 12
    }
  }
});

需要同时注意三件事:

要点原因
createFileRoute() 的路径要和文件位置匹配TanStack Router 会用它和生成的 route tree 建立类型关系。
后台页面放在 (admin)才会经过后台 layout、菜单、tabs 和权限守卫。
菜单、标题、权限写在 staticData静态模式菜单生成、面包屑、tabs 和权限判断都会消费这些元信息。

如果页面还需要目录级布局,增加 layout.tsx

pages/(admin)/report/layout.tsx
pages/(admin)/report/center.tsx
pages/(admin)/report/audit.tsx

layout.tsx 负责渲染 <Outlet />,并在 staticData.menu 中配置一级菜单;子页面配置自己的二级菜单。

动态参数路由

动态参数用 $ 前缀文件名表示。当前已有:

pages/(admin)/manage/user/$id.tsx

它对应的路由是:

createFileRoute('/(admin)/manage/user/$id')

可访问 URL 是:

/manage/user/123

详情页通常不出现在菜单里,而是让左侧菜单保持选中列表页:

staticData: {
  title: 'manage_user_detail',
  i18nKey: 'route.manage_user_$id',
  menu: {
    hide: true,
    activeMenu: '/manage/user'
  }
}

hide: true 只是不渲染菜单项;路由仍然会被记录到 quick reference map 中,activeMenu 会让菜单和面包屑回到指定父级。

静态模式和动态模式

路由模式由 .env 中的 VITE_AUTH_ROUTE_MODE 控制。当前默认值是:

VITE_AUTH_ROUTE_MODE=static

两种模式的差异在菜单和权限来源,不在页面组件来源:

模式菜单来源权限来源页面组件来源
static前端 route tree 中的 staticDataroute match 的 staticData.permissions前端 apps/admin/src/pages
dynamic后端 fetchGetBackendRoutes() 返回的路由树,经 loadAdminDynamicRoutes() 适配适配后的路由节点 permissions,再结合前端用户角色前端 apps/admin/src/pages

动态模式下,bootstrap.tsx 提供的 loadDynamicRoutes 指向:

loadAdminDynamicRoutes

接口原始响应类型在 packages/@core/types/src/api/route.d.ts 中定义:

interface BackendRouteResponse {
  home?: string | null;
  routes: BackendRoutePayload[];
}

apps/admin/src/features/menus/dynamic-routes.ts 会把后端 name/path/component/handle 响应适配成布局包消费的 Api.Route.BackendRoute[]。适配后才会进入和前端 staticData 接近的元信息结构,例如 titlei18nKeymenupermissionshrefurl

排查顺序

现象先检查
新增页面访问 404文件是否在 src/pages 下,createFileRoute() 路径是否和目录约定一致,routeTree.gen.ts 是否重新生成。
页面能访问但菜单不显示staticData.menu.hide 是否为 true,静态模式下是否有 staticData,动态模式下后端是否返回该 path。
菜单显示但点击 404动态菜单返回的 path 是否对应前端真实 TanStack Route。
切换 tabs 后表单值丢失route 是否配置 keepAlive,tab 是否仍打开,带查询参数页面是否需要 tab.multi
详情页打开后左侧菜单不选中隐藏页是否配置 menu.activeMenu,路径是否使用规范化后的可跳转路径,例如 /manage/user
登录后跳转到非预期页面.envVITE_ROUTE_HOME、后端动态路由返回的 homeuseMenus().home
进入后台直接 403VITE_AUTH_ROUTE_MODE,静态模式查 staticData.permissions,动态模式查后端路由树和用户角色。
修改路由文件后类型仍旧重新启动 Vite 或执行会触发 TanStack Router 生成的脚本,确认 routeTree.gen.ts 已更新。

常见误区

误区正确理解
动态路由模式可以让后端直接新增前端页面动态模式只改变菜单和授权数据来源,React 页面组件仍必须存在于前端 route tree。
routeTree.gen.ts 可以手动修路由这是生成文件,会被 TanStack Router 覆盖;要改页面文件或插件配置。
(admin) 会出现在 URL 里括号目录是路由组,只影响组织和 layout,不进入 URL。
modules 里的组件会变成路由当前插件明确忽略 componentsmodules 目录。
每个后台页面都要自己写登录守卫后台页面挂在 (admin) 下即可复用 (admin)/layout.tsxguardAdminRoute()

相关位置

主题继续查看
路由标题、菜单、图标、隐藏页、外链路由元信息
页面实例缓存、React Activity 和 tabs 切换验证路由缓存
登录拦截、用户初始化、外链路由守卫
403、静态/动态权限、超级角色权限
应用启动时如何注入 route tree 和菜单配置启动流程
目录职责和 app/package 边界项目结构

On this page