4、前端基础(三):React Router路由配置、嵌套路由、导航守卫

好,咱们今天聊聊 React Router。说实话,这是前端单页应用里最绕不开的一个库。你想想看,没有路由,页面怎么跳转?用户怎么在不同视图之间切换?我刚开始用 React 写项目时,第一件事就是配路由。今天我把这块掰开揉碎讲清楚。

4.1 路由基础配置

先说说最基础的。React Router 目前主流是 v6 版本,跟 v5 差别挺大。我个人习惯直接用 v6,因为 API 更简洁,嵌套路由也更直观。

安装很简单:

npm install react-router-dom

然后在项目入口文件里,用 BrowserRouter 把整个应用包起来:

import { BrowserRouter } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Main />
    </BrowserRouter>
  );
}

这里要注意,BrowserRouter 用的是 HTML5 的 history API,URL 看起来是正常的路径。还有一种 HashRouter,URL 里带 # 号,我一般只在静态部署时用。

接下来配置路由表。v6 里用 RoutesRoute

import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';

function AppRoutes() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/contact" element={<Contact />} />
    </Routes>
  );
}

嗯,这里有个细节:v6 里 component 属性改成了 element,直接传 JSX。我之前从 v5 迁移时踩过这个坑,一直报错找不到组件。

4.2 嵌套路由

嵌套路由是实际项目里用得最多的。比如一个后台管理系统,左侧是菜单,右侧是内容区。点击菜单切换右侧内容,左侧菜单不动。这就是典型的嵌套路由场景。

v6 里实现嵌套路由有两种方式。第一种是在路由配置里直接嵌套:

<Routes>
  <Route path="/dashboard" element={<DashboardLayout />}>
    <Route index element={<DashboardHome />} />
    <Route path="users" element={<Users />} />
    <Route path="settings" element={<Settings />} />
  </Route>
</Routes>

注意看,DashboardLayout 组件里需要有一个 <Outlet /> 占位符。子路由的内容会渲染在这个位置。

import { Outlet } from 'react-router-dom';

function DashboardLayout() {
  return (
    <div>
      <Sidebar />
      <main>
        <Outlet />  {/* 子路由内容显示在这里 */}
      </main>
    </div>
  );
}

第二种方式是用 useRoutes 钩子,适合把路由配置抽成单独文件:

import { useRoutes } from 'react-router-dom';

const routeConfig = [
  {
    path: '/dashboard',
    element: <DashboardLayout />,
    children: [
      { index: true, element: <DashboardHome /> },
      { path: 'users', element: <Users /> },
      { path: 'settings', element: <Settings /> },
    ],
  },
];

function AppRoutes() {
  return useRoutes(routeConfig);
}

我个人更推荐第二种。为什么?因为路由配置多了以后,单独维护一个文件,清晰很多。我在项目中遇到过,所有路由堆在 App.js 里,后来改一个路径要找半天。

小技巧:嵌套路由的路径是相对路径。比如 users 会自动拼接成 /dashboard/users。不要写成 /users,否则会匹配到根路径下的 users。

4.3 导航守卫

导航守卫,说白了就是「用户能不能访问这个页面」。比如未登录用户不能进个人中心,管理员才能进后台。

React Router v6 没有像 Vue Router 那样的 beforeEach 钩子。但我们可以自己封装一个高阶组件或者用 Navigate 组件来实现。

最简单的做法:

import { Navigate } from 'react-router-dom';

function ProtectedRoute({ children }) {
  const isAuthenticated = localStorage.getItem('token');
  
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }
  
  return children;
}

然后在路由里包裹需要保护的组件:

<Route
  path="/dashboard"
  element={
    <ProtectedRoute>
      <DashboardLayout />
    </ProtectedRoute>
  }
>
  {/* 子路由 */}
</Route>

这里有个坑:replace 属性很重要。如果不加,用户点浏览器的返回按钮,会回到登录页,然后又重定向到 dashboard,形成死循环。我曾经因为这个被测试小姐姐追着改 bug。

更复杂的场景,比如角色权限控制:

function RoleGuard({ children, allowedRoles }) {
  const userRole = getUserRole(); // 从 context 或 store 获取
  
  if (!allowedRoles.includes(userRole)) {
    return <Navigate to="/403" replace />;
  }
  
  return children;
}

// 使用
<Route
  path="/admin"
  element={
    <RoleGuard allowedRoles={['admin', 'superadmin']}>
      <AdminPanel />
    </RoleGuard>
  }
/>
注意:前端守卫只是用户体验层面的保护。真正的权限校验必须在后端做。前端可以绕过,后端不能。我见过有人只在前端做权限判断,结果被直接调接口搞崩了。

4.4 编程式导航

有时候需要在代码里跳转,比如登录成功后跳回首页。v6 里用 useNavigate 钩子:

import { useNavigate } from 'react-router-dom';

function LoginPage() {
  const navigate = useNavigate();
  
  const handleLogin = async () => {
    await login();
    navigate('/dashboard', { replace: true });
  };
  
  return (
    <button onClick={handleLogin}>登录</button>
  );
}

这里 replace: true 的意思是替换当前历史记录,而不是新增一条。登录成功后用户按返回键不会回到登录页,体验更好。

还可以传 state:

navigate('/profile', { state: { from: 'settings' } });

目标页面用 useLocation 接收:

import { useLocation } from 'react-router-dom';

function Profile() {
  const location = useLocation();
  console.log(location.state); // { from: 'settings' }
}

4.5 路由懒加载

项目大了以后,所有页面打包成一个 JS 文件,首屏加载会很慢。这时候需要路由懒加载。

React 提供了 lazySuspense

import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Users = lazy(() => import('./pages/Users'));

function AppRoutes() {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/users" element={<Users />} />
      </Routes>
    </Suspense>
  );
}

注意,lazy 只支持默认导出。如果你的组件是命名导出,需要转一下:

const Dashboard = lazy(() => 
  import('./pages/Dashboard').then(module => ({ default: module.Dashboard }))
);

核心要点总结:

  • v6 用 Routes + Routecomponent 改成 element
  • 嵌套路由用 <Outlet /> 占位
  • 导航守卫封装成组件,用 <Navigate /> 重定向
  • 编程式导航用 useNavigate 钩子
  • 懒加载用 lazy + Suspense

好了,路由这块就这些。说白了就是配置路径、控制访问、按需加载。你把这些搞明白,大部分项目的路由需求都能应付。下一节我们聊聊状态管理,那又是另一个有意思的话题。