React 路由与应用结构:嵌套路由、布局组件与权限控制
路由不只是「几个 URL 对应几个页面」,在稍大的应用里,它往往直接影响整体结构:布局怎么拆、状态放哪、权限怎么管。
路由与布局:Outlet 和布局组件
以 React Router 为例,可以用嵌套路由来表达「某一片区域共用一个布局」:
1const router = createBrowserRouter([
2 {
3 path: '/',
4 element: <RootLayout />,
5 children: [
6 { index: true, element: <HomePage /> },
7 {
8 path: 'dashboard',
9 element: <DashboardLayout />,
10 children: [
11 { index: true, element: <DashboardHome /> },
12 { path: 'reports', element: <ReportsPage /> },
13 ],
14 },
15 ],
16 },
17]);
布局组件里通过 <Outlet> 渲染子路由:
1function RootLayout() {
2 return (
3 <div className="app">
4 <Sidebar />
5 <main>
6 <Outlet />
7 </main>
8 </div>
9 );
10}
11
12function DashboardLayout() {
13 return (
14 <div className="dashboard">
15 <DashboardHeader />
16 <div className="dashboard-body">
17 <Outlet />
18 </div>
19 </div>
20 );
21}
这样可以清楚地把「全局布局」「某个子区域布局」各自独立出来。
URL 即状态:让可以分享的状态进路由
凡是需要「复制链接给别人,看到的是同一个视角」的东西,适合放进 URL:
- 当前选中的资源 id;
- 列表的筛选条件、页码、排序规则;
- 某个 Tab 或视图模式。
在 React Router 中可以用 search params 来承载:
1function UserListPage() {
2 const [searchParams, setSearchParams] = useSearchParams();
3 const page = Number(searchParams.get('page') || '1');
4 const keyword = searchParams.get('q') || '';
5
6 function updateFilter(next: { page?: number; keyword?: string }) {
7 const params: Record<string, string> = {};
8 params.page = String(next.page ?? page);
9 params.q = next.keyword ?? keyword;
10 setSearchParams(params);
11 }
12
13 // ...
14}
这比把这些状态只放在组件里多一个好处:刷新和分享都不会丢。
权限控制:路由守卫与 UI 级别的限制
权限相关的逻辑通常可以分成两层:
- 路由层:是否允许访问某个页面;
- UI 层:是否展示某个按钮/入口。
路由层的保护(Protected Route)
可以写一个简单的保护组件:
1function RequireAuth({ children }: { children: React.ReactElement }) {
2 const user = useCurrentUser();
3 const location = useLocation();
4
5 if (!user) {
6 return <Navigate to="/login" state={{ from: location }} replace />;
7 }
8
9 return children;
10}
在路由表中使用:
1{
2 path: 'settings',
3 element: (
4 <RequireAuth>
5 <SettingsPage />
6 </RequireAuth>
7 ),
8}
UI 层的权限
对于按钮、菜单项之类,通常不会直接藏掉全部,而是带上合适的禁用/说明:
1function DeleteButton({ resourceId }: { resourceId: string }) {
2 const user = useCurrentUser();
3 const canDelete = user?.permissions.includes('delete_resource');
4
5 if (!canDelete) {
6 return (
7 <button disabled title="没有删除权限">
8 删除
9 </button>
10 );
11 }
12
13 return <button onClick={() => deleteResource(resourceId)}>删除</button>;
14}
页面是否能进、按钮能否点,一般会分别控制,避免单点失效。
路由与状态管理的边界
路由只适合承载「序列化后能放进 URL」的那部分状态,其它状态可以留给:
- 本地 state(局部 UI 行为);
- Context(当前用户、布局设置等);
- 远程状态库(服务器数据)。
可以先画出几个层次:
- 顶层:路由树 + 布局组件;
- 中间层:按模块划分的页面和 Context;
- 底层:各自负责的 UI 组件和业务 Hooks。
路由主要管「页面怎么切、布局怎么嵌」,避免把所有非 URL 状态都丢进路由相关逻辑。
小结
- 用嵌套路由和布局组件,可以把应用结构表达得足够清楚:哪些区域共用布局、哪些是独立页面。
- URL 是天然的共享载体,适合放筛选条件、选中项这一类「应该能被分享/刷新保留」的状态。
- 权限控制分路由层和 UI 层两级处理,既防止非法访问,又给用户合理的可见反馈。
- 路由负责结构和可分享状态,其它本地 UI 状态和服务器状态交给各自更合适的工具来管理。