单页面应用(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+

983

被折叠的 条评论
为什么被折叠?



