路由和菜单
演示项目为了可以部署到 GitHub,路由模式为 Hash 模式,如果你需要 History 模式,请修改
.env.production
文件中的变量VITE_ROUTER_MODE
为history
。
项目使用 React Router 的数据路由生成路由,并根据路由自动生成菜单。
TIP
除了初次进入系统,即登录页面,项目中禁止使用 navigate("/")
直接跳转到首页,这会重新渲染这个应用,请使用 navigate(import.meta.env.VITE_BASE_HOME_PATH)
替代。
路由目录
src/router
├── constants.ts # 路由常量
├── utils # 路由工具
├── types # 路由类型
│ │ ├── guard # 路由守卫
│ │ │ ├── auth-guard.tsx # 权限守卫
│ │ │ ├── common-guard.ts # 通用守卫(加载顶部动画)
│ │ │ ├── guard.ts # 创建守卫
│ │ │ └── index.ts # 路由守卫聚合
├── index.ts # 路由配置文件
├── extra-info
│ ├── order # 路由顺序决定菜单排序
│ └── index.ts # 聚合导出
└── routes
├── core # 核心路由
├── external # 外部路由
├── modules # 动态路由
├── static # 静态路由
└── index.ts # 聚合路由
路由类型
路由分为四大类:核心路由、外部路由、静态路由和动态路由。
- 核心路由是应用必须的包含的路由,例如根路由、登录路由、404 路由、403 路由等;
- 外部路由即不需要登录和权限认证的路由,例如网站的文档、用户协议、隐私政策等;
- 静态路由或者叫前端路由,是在项目启动时就已经确定的路由;
- 动态路由或者叫后端路由,在用户登录后,根据接口返回的结果动态生成的路由。
核心路由
核心路由是应用必须的包含的路由,例如根路由、登录路由、404 路由等;核心路由的配置在应用下 src/router/routes/core
目录下。
项目中,请设置核心路由在菜单中隐藏(
hideInMenu = true
)
TIP
核心路由主要用于框架的基础功能,业务逻辑路由请放在静态路由或动态路由中。
外部路由
外部路由即不需要登录和权限认证的路由,例如网站的文档、用户协议、隐私政策等;外部路由的配置在应用下 src/router/routes/external
目录下。
静态路由
静态路由存储在 src/router/routes/static
目录下,如果不需要静态路由,保持这个文件夹为空即可。
动态路由
动态路由存储在 src/router/routes/modules
目录下。
- 路由仅为调用后端获取的情况下,即
设置 enableBackendAccess 为 true 、enableFrontendAceess 为 false
,此目录仅仅作为备份,并不会被读取。 - 路由仅为前端获取的情况下,即
设置 enableBackendAccess 为 false 、enableFrontendAceess 为 true
,则会读取此目录下的路由。
路由获取的方式
查看权限控制章节,请查看 权限。
路由 id
默认情况下,路由的 id
是可选的,为了使用方便,框架使用路由的 path 作为路由的 id,例如: 路由 { "path": "/about" }
,生成的路由对象是 { "path": "/about", "id": "/about" }
。
INFO
其中根路由的 id 是 root-route
。
路由懒加载
React Router 的路由懒加载 —— Lazy 需要组件具名导出一个 Component
属性。
例如:
export const Component = () => <div>About</div>;
但项目中的组件都是默认导出,即使是使用具名导出也是如下形式:
export const About = () => <div>About</div>;
所以不建议使用路由自带的懒加载 API。
下面演示如何使用路由懒加载,加载默认导出的文件:
路由懒加载,加载默认导出的文件
import type { AppRouteRecordRaw } from "#src/router/types";
import { $t } from "#src/locales";
import { about } from "#src/router/extra-info";
import { UserOutlined } from "@ant-design/icons";
import { createElement } from "react";
const routes: AppRouteRecordRaw[] = [
{
path: "/about",
lazy: async () => {
const About = await import("#src/pages/about");
return { Component: About.default };
},
handle: {
order: about,
title: $t("common.menu.about"),
icon: createElement(UserOutlined),
},
},
];
export default routes;
推荐使用 React 自带的 lazy
函数,这样就可以加载默认导出的文件了。
推荐使用路由懒加载的方式
import type { AppRouteRecordRaw } from "#src/router/types";
import { ContainerLayout } from "#src/layout";
import { $t } from "#src/locales";
import { about } from "#src/router/extra-info";
import { UserOutlined } from "@ant-design/icons";
import { createElement, lazy } from "react";
const About = lazy(() => import("#src/pages/about"));
const routes: AppRouteRecordRaw[] = [
{
path: "/about",
Component: About,
handle: {
order: about,
title: $t("common.menu.about"),
icon: createElement(UserOutlined),
},
},
];
export default routes;
WARNING
React 的 lazy 一定要放在模块的顶部,否则使用的方式是无效的,参见 My lazy component’s state gets reset unexpectedly
索引路由
如果使用了 索引路由 请把权限、图标等信息写在 index
路由中,这样在生成菜单时,会自动获取到权限、图标等信息。
索引路由示例代码
import type { AppRouteRecordRaw } from "#src/router/types";
import { ContainerLayout } from "#src/layout";
import { $t } from "#src/locales";
import { home } from "#src/router/extra-info";
import { HomeOutlined } from "@ant-design/icons";
import { createElement, lazy } from "react";
const Home = lazy(() => import("#src/pages/home"));
const routes: AppRouteRecordRaw[] = [
{
path: "/home",
Component: ContainerLayout,
handle: {
order: home,
title: $t("common.menu.home"),
icon: createElement(HomeOutlined),
},
children: [
{
index: true,
Component: Home,
handle: {
title: $t("common.menu.home"),
icon: createElement(HomeOutlined),
},
},
],
},
];
export default routes;
路由的排序
路由的排序,默认是按照 handle.order
属性进行排序的,handle.order
只在路由的第一层级生效,生效的 order 会决定生成菜单的顺序。
路由的排序示例代码
import type { AppRouteRecordRaw } from "#src/router/types";
import { ContainerLayout } from "#src/layout";
import { $t } from "#src/locales";
import { home } from "#src/router/extra-info";
import { HomeOutlined } from "@ant-design/icons";
import { createElement, lazy } from "react";
const Home = lazy(() => import("#src/pages/home"));
const routes: AppRouteRecordRaw[] = [
{
path: "/home",
Component: ContainerLayout,
handle: {
order: home,
title: $t("common.menu.home"),
icon: createElement(HomeOutlined),
},
children: [
{
index: true,
Component: Home,
handle: {
title: $t("common.menu.home"),
icon: createElement(HomeOutlined),
},
},
],
},
];
export default routes;
新增路由
新增路由的步骤如下:
- 添加路由文件
- 添加页面组件
添加路由
在对应的路由文件夹下添加一个路由文件,如下:
需要登录才能访问的路由中,必须在根路由中使用 ContainerLayout 组件,否则会导致菜单无法正常显示。所以一级路由需要嵌套在 children 中,并设定 index = true
import type { AppRouteRecordRaw } from "#src/router/types";
import { ContainerLayout } from "#src/layout";
import { $t } from "#src/locales";
import { home } from "#src/router/extra-info";
import { HomeOutlined } from "@ant-design/icons";
import { createElement, lazy } from "react";
const Home = lazy(() => import("#src/pages/home"));
const routes: AppRouteRecordRaw[] = [
{
path: "/home",
Component: ContainerLayout,
handle: {
order: home,
title: $t("common.menu.home"),
icon: createElement(HomeOutlined),
},
children: [
{
index: true,
Component: Home,
handle: {
title: $t("common.menu.home"),
icon: createElement(HomeOutlined),
},
},
],
},
];
export default routes;
添加页面组件
在 #/pages/home/
目录下,新增文件 index.tsx
文件, 内容如下:
export default function Home() {
return <h1>Home Page</h1>;
}
验证
页面添加完成,访问 http://localhost:3333/home/
即可看到新的页面。
路由配置
每个路由目录下文件内容的结构和 React Router 的 Route 路由配置格式一致。
INFO
详细路由教程建议通读 React Router 的 Route 章节。
下面只讲解本项目增加的功能如何配合路由文件一起使用,新增的功能,只是扩展了路由对象上的字段,没有任何不兼容的地方。
路由配置项主要在对象路由的 handle
属性中,以下是常用的配置项:
const routes = [
{
path: "/home",
Component: ContainerLayout,
handle: {
order: home,
title: $t("common.menu.home"),
icon: createElement(HomeOutlined),
},
},
];
路由 handle 的类型:
export interface RouteMeta {
/**
* 路由标题,通常用于页面标题或者侧边栏菜单显示
*/
title: ReactNode
/**
* 设置页面是否开启缓存,开启后页面会缓存,不会重新加载,仅在标签页启用时有效。
* @default true
*/
keepAlive?: boolean
/**
* 是否在菜单中隐藏,用于控制某些路由不在侧边栏菜单中显示
*/
hideInMenu?: boolean
/**
* 菜单图标,用于侧边栏菜单项的图标显示
*/
icon?: ReactNode
/**
* 菜单排序,用于控制侧边栏菜单的显示顺序
*/
order?: number
/**
* 用于配置页面的权限,只有拥有对应权限的用户才能访问页面,不配置则不需要权限。
*/
roles?: string[]
/**
* 页面内按钮级别的权限,用于控制页面内按钮的显示和隐藏
*/
permissions?: string[]
/**
* iframe链接,如果路由需要在iframe中加载外部页面时使用
*/
iframeLink?: string
/**
* 外部链接,点击后直接在新标签页中打开
*/
externalLink?: string
/**
* 用于配置页面是否忽略权限,直接可以访问
*/
ignoreAccess?: boolean
/**
* @description 指定当前激活的菜单,适用于动态路由情景下激活父菜单
* @example 从父路由 '/user/info' 导航到子路由 '/user/info/1' 时,可以手动手动指定以高亮显示父菜单 '/user/info'
*/
currentActiveMenu?: string
/**
* 当前路由为请求后端接口得到的
*/
backstage?: boolean
}
title
- 类型:
ReactNode
- 默认值:
undefined
路由标题,通常用于页面标题或者侧边栏菜单显示。
icon
- 类型:
ReactNode
- 默认值:
undefined
菜单图标,用于侧边栏菜单项的图标显示。
order
- 类型:
number
- 默认值:
0
菜单排序,用于控制侧边栏菜单的显示顺序。
roles
- 类型:
string[]
- 默认值:
undefined
用于配置页面的权限,只有拥有对应权限的用户才能访问页面,不配置则不需要权限。
permissions
- 类型:
string[]
- 默认值:
undefined
页面内按钮级别的权限,用于控制页面内按钮的显示和隐藏。
keepAlive
- 类型:
boolean
- 默认值:
true
设置页面是否开启缓存,开启后页面会缓存,不会重新加载,仅在标签页启用时有效。
hideInMenu
- 类型:
boolean
- 默认值:
false
是否在菜单中隐藏,用于控制某些路由不在侧边栏菜单中显示。
iframeLink
- 类型:
string
- 默认值:
undefined
iframe 链接,如果路由需要在 iframe 中加载外部页面时使用。
externalLink
- 类型:
string
- 默认值:
undefined
外部链接,点击后直接在新标签页中打开。
ignoreAccess
- 类型:
boolean
- 默认值:
false
用于配置页面是否忽略权限,直接可以访问。
currentActiveMenu
- 类型:
string
- 默认值:
undefined
指定当前激活的菜单,适用于动态路由情景下激活父菜单。
backstage
- 类型:
boolean
- 默认值:
false
当前路由为请求后端接口得到的。