React Router 单页面路由管理:createBrowserRouter 实战

单页面应用(SPA)只有一个 HTML 页面,页面切换由 JavaScript 完成,浏览器不重新加载整页。React Router 根据 URL 渲染对应组件,并在地址变化时保持 SPA 无刷新体验。

本文以 React Router v6.4+ 的 createBrowserRouter 为主线,覆盖路由配置、导航、动态参数与路由守卫。前置环境:Node.js + Vite React 项目,安装 react-router-dom。

npm create vite@latest my-app -- --template react
cd my-app && npm install && npm install react-router-dom

一、核心概念

理解 React Router,先弄清 URL 与 UI 之间的对应关系。

Route(路由规则)
定义「某个 URL 应该渲染哪个组件」。例如 path="/home" 对应 Home 组件。路由表就是一张映射表,Router 按当前 URL 查表决定渲染内容。

Router(路由器)
监听浏览器地址变化(前进、后退、编程式跳转),并在变化时重新匹配路由、更新组件树。没有 Router,Link 和 navigate 都无法工作。

Navigation(导航)
改变 URL 的手段。声明式用 Link(写在 JSX 里),编程式用 useNavigate(在事件或逻辑里调用)。二者都只改 URL,不触发整页刷新。

动态路由
URL 中含可变段,如 /user/:id 中的 :id。同一套页面组件可根据不同参数展示不同数据,是详情页、编辑页的常见模式。

路由守卫
在真正渲染目标页面前做权限校验。未登录访问后台、普通用户访问管理员页等场景,不满足条件则重定向到登录页或 403 页,而不是先渲染再隐藏。

二、路由配置(重点)

为什么用 createBrowserRouter
───────────────────────────
React Router 有两套常见写法:

旧式:BrowserRouter + Routes + Route,路由分散在各组件 JSX 中。
新式:createBrowserRouter + RouterProvider,路由表集中在一个文件。

v6.4+ 推荐后者,原因有三:
1. 路由结构一目了然,维护大型项目时不必在多个文件间跳转;
2. 天然支持 loader、action 等数据层 API(本文不展开,但扩展时无需改架构);
3. 嵌套路由、404 兜底等配置方式更统一。

两套 API 不要混用:选了 createBrowserRouter,就不要再包 BrowserRouter。

路由表

// src/router/index.jsx
import { createBrowserRouter } from 'react-router-dom';
import Layout from '../layout/Layout';
import Home from '../pages/Home';
import About from '../pages/About';
import NotFound from '../pages/NotFound';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      { index: true, element: <Home /> },
      { path: 'about', element: <About /> },
    ],
  },
  { path: '*', element: <NotFound /> },
]);

export default router;

字段含义:
path: '/'        父路由路径
element         匹配该路径时渲染的组件
children        嵌套子路由;子 path 相对父 path 拼接('about' → /about)
index: true     访问父路径时的默认子页面,等价于 path: '' 且为默认匹配
path: '*'       通配符,匹配所有未定义路径,作 404 兜底,须放在路由表末尾

入口挂载

createBrowserRouter 返回的是 router 对象,须通过 RouterProvider 注入应用:

// src/main.jsx
import { RouterProvider } from 'react-router-dom';
import router from './router';

ReactDOM.createRoot(document.getElementById('root')).render(
  <RouterProvider router={router} />
);

RouterProvider 相当于整个应用的路由上下文提供者,其内部的组件才能使用 Link、useNavigate、useParams 等 Hook。

嵌套路由与 Outlet

嵌套路由解决「多页共享同一布局」的问题:导航栏、侧边栏只写一次,中间内容区随 URL 切换。父路由渲染 Layout,子路由渲染具体页面,二者通过 Outlet 衔接——Outlet 是子路由的渲染插槽。

// src/layout/Layout.jsx
import { Outlet, Link } from 'react-router-dom';

export default function Layout() {
  return (
    <div>
      <nav>
        <Link to="/">首页</Link> | <Link to="/about">关于</Link>
      </nav>
      <hr />
      <Outlet />   {/* 子路由组件渲染在这里 */}
    </div>
  );
}

Link 与原生 <a> 的区别:Link 拦截点击,只改 URL 不请求新 HTML,保持 SPA 体验。嵌套路由配置了 children 时,父组件必须包含 Outlet,否则子页面区域为空白。

部署注意
createBrowserRouter 使用 History API,URL 形如 /about,不带 #。开发时 Vite 已做 fallback;生产环境若直接刷新 /about,服务器可能因找不到对应静态文件而返回 404。解决方式:配置服务器将所有请求回退到 index.html(如 Nginx 的 try_files),或改用 createHashRouter(URL 变为 /#/about)。

三、导航

声明式 vs 编程式
───────────────
声明式导航把「去哪里」写在 JSX 里,适合菜单、文字链接等固定入口:

<Link to="/about">关于我们</Link>

编程式导航在逻辑执行后再决定跳转,适合登录成功、表单校验通过、倒计时结束等场景:

// src/pages/Login.jsx
import { useNavigate } from 'react-router-dom';

export default function Login() {
  const navigate = useNavigate();

  const handleLogin = () => {
    localStorage.setItem('token', 'fake-token');
    navigate('/', { replace: true });
  };

  return <button onClick={handleLogin}>登录</button>;
}

navigate 的 replace 选项
replace: true  — 用新记录替换当前历史栈条目,用户点「后退」不会回到被替换的页
replace: false(默认)— 压入新历史记录,可后退

登录成功后通常用 replace,避免用户按后退键又回到登录页。

注意:
站内跳转只用 Link 或 navigate,不要用 window.location.href(会整页刷新,丢失 React 状态)。
useNavigate 必须在 RouterProvider 子树内调用。
navigate 的 state 参数(如 state={{ from: '/dashboard' }})存在内存中,刷新页面后会丢失;需要持久化的信息应放 URL 参数或 localStorage。

四、动态路由

路径参数与查询参数
──────────────────
动态 URL 有两种常见形式,适用场景不同:

路径参数  /user/42          — 标识资源本身,通常必填,REST 风格
查询参数  /user/42?tab=posts  — 可选的筛选、分页、Tab 状态

路由表配置:

{ path: 'user/:id', element: <UserDetail /> },
{ path: 'products/:category/:id', element: <ProductDetail /> },

组件内读取:

// src/pages/UserDetail.jsx
import { useParams, useSearchParams } from 'react-router-dom';

export default function UserDetail() {
  const { id } = useParams();                    // 路径参数,值均为字符串
  const [searchParams, setSearchParams] = useSearchParams();
  const tab = searchParams.get('tab');           // 查询参数 ?tab=posts

  return (
    <div>
      <p>用户 ID: {id}</p>
      <p>Tab: {tab}</p>
    </div>
  );
}

跳转方式:
<Link to={`/user/${userId}`}>
navigate(`/user/${id}?tab=posts`)

组件复用与数据请求
从 /user/1 切到 /user/2 时,UserDetail 组件不会卸载重建,只是 props 和 URL 变了。因此 useEffect 拉取数据的依赖数组必须包含 id,否则不会重新请求。

setSearchParams 的行为
直接传入新对象会覆盖原有查询参数。若要保留其他参数,需先读取再合并:

setSearchParams(prev => {
  prev.set('page', '2');
  return prev;
});

五、路由守卫(重点)

守卫要解决的核心问题
────────────────────
某些页面只有登录用户或特定角色才能访问。守卫在渲染目标组件前拦截请求,校验失败则立即重定向,避免未授权内容短暂闪现。

关键原则:守卫逻辑必须写在 React 组件内部,每次渲染时读取最新登录态。不可在 createBrowserRouter([...]) 调用时一次性判断——那时应用刚启动,用户尚未登录,且后续登录状态变化也不会触发路由表重新创建。

单页守卫:ProtectedRoute

适合个别页面需要保护的场景。ProtectedRoute 是包装组件:通过则渲染 children,否则返回 Navigate 做重定向。

// src/components/ProtectedRoute.jsx
import { Navigate, useLocation } from 'react-router-dom';

export default function ProtectedRoute({ children }) {
  const location = useLocation();
  const isLoggedIn = localStorage.getItem('token') !== null;

  if (!isLoggedIn) {
    return (
      <Navigate to="/login" replace state={{ from: location.pathname }} />
    );
  }
  return children;
}

路由表用法:

{
  path: 'dashboard',
  element: (
    <ProtectedRoute>
      <Dashboard />
    </ProtectedRoute>
  ),
}

state={{ from: location.pathname }} 的作用
记录用户原本想访问的路径。登录成功后可以跳回该页,而不是固定跳首页。

登录回跳

// Login.jsx 中配合 useLocation 读取 from
const location = useLocation();
const from = location.state?.from || '/';

const handleLogin = () => {
  localStorage.setItem('token', 'fake-token');
  navigate(from, { replace: true });
};

整组守卫:AuthLayout

当某一模块下所有子页面都需要登录(如整个 /admin 后台),逐路由包 ProtectedRoute 会重复代码。此时用 AuthLayout 作为父路由 element,在内部统一校验,通过则渲染 Outlet 供子路由展示。

// src/layout/AuthLayout.jsx
export default function AuthLayout() {
  const location = useLocation();
  if (!localStorage.getItem('token')) {
    return <Navigate to="/login" replace state={{ from: location.pathname }} />;
  }
  return <Outlet />;
}

// 路由
{
  path: '/admin',
  element: <AuthLayout />,
  children: [
    { path: 'users', element: <UserList /> },
    { path: 'settings', element: <Settings /> },
  ],
}

ProtectedRoute 与 AuthLayout 的选择
单页保护 → ProtectedRoute 包单个 element
整组保护 → AuthLayout + Outlet 包一组 children
角色细分 → 在守卫组件内增加 role 判断,或拆成 AdminRoute、GuestRoute

生产环境补充
守卫重定向应带 replace,避免历史栈中残留被拦截的页面。若 token 需向服务器异步校验,校验完成前先渲染 loading,再决定展示内容或跳转,防止未授权内容闪烁。

六、完整路由表示例

目录结构:

src/
  main.jsx
  router/index.jsx
  layout/Layout.jsx
  components/ProtectedRoute.jsx
  pages/Home.jsx About.jsx Login.jsx Dashboard.jsx UserDetail.jsx NotFound.jsx

// src/router/index.jsx
import { createBrowserRouter } from 'react-router-dom';
import Layout from '../layout/Layout';
import ProtectedRoute from '../components/ProtectedRoute';
import Home from '../pages/Home';
import About from '../pages/About';
import Login from '../pages/Login';
import Dashboard from '../pages/Dashboard';
import UserDetail from '../pages/UserDetail';
import NotFound from '../pages/NotFound';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      { index: true, element: <Home /> },
      { path: 'about', element: <About /> },
      { path: 'login', element: <Login /> },
      { path: 'user/:id', element: <UserDetail /> },
      {
        path: 'dashboard',
        element: (
          <ProtectedRoute>
            <Dashboard />
          </ProtectedRoute>
        ),
      },
    ],
  },
  { path: '*', element: <NotFound /> },
]);

export default router;

七、API 速查

配置    createBrowserRouter / RouterProvider / Outlet
导航    Link / useNavigate / Navigate
读 URL  useParams / useSearchParams / useLocation
守卫    ProtectedRoute(单页)/ AuthLayout + Outlet(整组)

主线归纳

createBrowserRouter 集中配置嵌套路由,Layout + Outlet 复用布局 → Link / navigate 实现无刷新导航 → :id + useParams 处理动态页 → ProtectedRoute / AuthLayout + Navigate 在组件内做权限守卫。
文档版本:React Router v6.4+
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值