React 路由与应用结构:嵌套路由、布局组件与权限控制

路由不只是「几个 URL 对应几个页面」,在稍大的应用里,它往往直接影响整体结构:布局怎么拆、状态放哪、权限怎么管。

以 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:

  • 当前选中的资源 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 层:是否展示某个按钮/入口。

可以写一个简单的保护组件:

 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}

对于按钮、菜单项之类,通常不会直接藏掉全部,而是带上合适的禁用/说明:

 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 状态和服务器状态交给各自更合适的工具来管理。