React Router v7 实战全景:路由配置、嵌套路由、鉴权守卫与懒加载完整指南

摘要

拆解BrowserRouter、Outlet嵌套路由、useParams、ProtectRoute鉴权、useNavigate导航、lazy懒加载,覆盖React Router v7全场景。


前两篇文章分别从零实现了 HashRouter 和 HistoryRouter,理解了 hashchangepushStatepopstate 这些底层机制。但在实际项目中,不会每次都手写路由------React Router 将这些底层能力封装成了声明式、组件化的 API,是 React 生态中前端路由的事实标准。

本文基于一个 React 19 + react-router-dom v7 的完整项目,逐层拆解 React Router 的核心能力:从基础的路由配置,到嵌套路由、动态参数、鉴权守卫、懒加载,覆盖生产级 SPA 中的全场景需求。

项目骨架与依赖

json 复制代码
{
  "dependencies": {
    "react": "^19.2.6",
    "react-dom": "^19.2.6",
    "react-router-dom": "^7.18.2"
  },
  "devDependencies": {
    "vite": "^8.0.12",
    "@vitejs/plugin-react": "^6.0.1"
  }
}

项目使用 Vite 8 作为构建工具,入口是标准的 index.htmlsrc/main.jsx。页面组件统一放在 pages/ 目录下,每个页面一个文件,路由守卫和导航组件独立管理。

BrowserRouter:选择 History 模式

React Router 提供了两种顶层路由容器:

jsx 复制代码
import { BrowserRouter as Router } from 'react-router-dom';

BrowserRouter 底层使用 History API(pushState + popstate),URL 干净无 # 号。与之对应的 HashRouter 则使用 hashchange。生产项目中,面向用户的 C 端产品几乎都选择 BrowserRouter------URL 美观、SEO 友好,代价是需要服务端配置 fallback(所有路由返回 index.html)。

BrowserRouter 包裹整个应用,内部的 RoutesRoute 组件才能正常工作。

Routes 与 Route:声明式路由配置

React Router 的核心设计理念是路由即组件。路由配置不是 JSON 也不是独立文件,而是直接写在 JSX 中:

jsx 复制代码
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  <Route path="/user/:id" element={<UserProfile />} />
  <Route path="/products" element={<Products />}>
    <Route path=":productId" element={<ProductDetail />} />
    <Route path="new" element={<NewProduct />} />
  </Route>
  <Route path="/login" element={<Login />} />
  <Route path="/pay" element={
    <ProtectRoute>
      <Pay />
    </ProtectRoute>
  } />
  <Route path="*" element={<NotFound />} />
</Routes>

Routes 是路由匹配引擎,它遍历所有子 Route,找到第一个匹配当前 URL 的路径,渲染对应的 element。同一时刻有且只有一个 Route 被激活。

Routepath 属性指定匹配规则,element 属性指定渲染内容。element 可以是任意 React 组件,这就为路由守卫、布局组件等模式提供了灵活的基础。

动态路由参数:useParams

URL 中经常需要携带动态参数,比如用户 ID、商品 ID。React Router 使用 :paramName 语法定义动态段:

jsx 复制代码
<Route path="/user/:id" element={<UserProfile />} />

在组件内部,通过 useParams Hook 获取参数值:

jsx 复制代码
import { useParams } from 'react-router-dom';

function UserProfile() {
  let { id } = useParams();
  return <h2>User Profile:{id}</h2>;
}

当用户访问 /user/123 时,id 的值为 "123"useParams 返回的是一个对象,键名对应路径中 :paramName 的名称。注意返回值始终是字符串类型,如需数字需要自行转换。

嵌套路由与 Outlet

嵌套路由是 React Router 最强大的特性之一。当页面存在"布局 + 子内容"的结构时------比如产品列表页顶部有统一的标题和筛选栏,下方根据 URL 切换详情页或新增页------嵌套路由是天然的选择。

jsx 复制代码
// 父路由组件
import { Outlet } from 'react-router-dom';

const Products = () => {
  return (
    <>
      <h1>产品列表</h1>
      <Outlet />
    </>
  );
};

Outlet 是嵌套路由的"插槽"------子路由匹配成功后,对应的组件会渲染在 Outlet 所在的位置。

jsx 复制代码
// 路由配置:子路由 path 相对于父路由
<Route path="/products" element={<Products />}>
  <Route path=":productId" element={<ProductDetail />} />
  <Route path="new" element={<NewProduct />} />
</Route>

访问 /products/123 时,Products 组件渲染,Outlet 处显示 ProductDetail;访问 /products/new 时,Outlet 处显示 NewProduct。父路由的 UI(标题、布局)保持不变,只有子区域切换。

子路由的 path相对于父路由 的,不需要写 /products/:productId,直接写 :productId 即可。

在 SPA 中,不能直接使用 <a> 标签做页面跳转------<a> 会触发浏览器的完整页面请求,导致 SPA 状态丢失。React Router 提供了 Link 组件替代:

jsx 复制代码
import { Link } from 'react-router-dom';

<nav>
  <ul>
    <li><Link to="/">Home</Link></li>
    <li><Link to="/about">About</Link></li>
    <li><Link to="/user/123">小家</Link></li>
    <li><Link to="/products/123">商品详情</Link></li>
  </ul>
</nav>

Link 组件渲染为 <a> 标签,但 to 属性被拦截处理------点击时调用 history.pushState 改变 URL,由 React Router 接管渲染,页面不会刷新。

编程式导航:useNavigate 与 useLocation

有些场景不适合用 Link------比如表单提交后跳转、超时后自动跳转、登录成功后跳转。这时需要编程式导航

jsx 复制代码
import { useNavigate, useLocation } from 'react-router-dom';

const Login = () => {
  const navigate = useNavigate();
  const location = useLocation();
  const from = location.state?.from || '/';

  function handleSubmit(e) {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const username = formData.get('username');
    const password = formData.get('password');

    if (username === 'admin' && password === '123456') {
      localStorage.setItem('isLogin', 'true');
      navigate(from, { replace: true });
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" placeholder="请输入用户名" required />
      <input name="password" placeholder="请输入密码" required />
      <button type="submit">登录</button>
    </form>
  );
};

useNavigate 返回一个跳转函数,调用 navigate('/path') 等价于 history.pushState + 渲染。第二个参数 { replace: true } 使用 replaceState 替换当前历史记录------登录成功后,用户点击后退按钮不会回到登录页,而是直接回到登录前的页面,体验更自然。

useLocation 返回当前 URL 的 location 对象,其中 location.state 携带了路由跳转时传递的额外数据。配合路由守卫,可以实现"从未登录页跳转到登录页,登录后自动回到原页面"的完整闭环。

useNavigate 也适用于定时跳转场景:

jsx 复制代码
const NotFound = () => {
  let navigate = useNavigate();
  useEffect(() => {
    setTimeout(() => navigate('/'), 3000);
  }, []);
  return <>Not Found</>;
};

路由守卫:ProtectRoute 组件

某些页面需要登录后才能访问(如支付页、个人中心)。React Router 没有内置的"路由守卫"概念,但利用其组件化特性,可以轻松实现:

jsx 复制代码
import { Navigate } from 'react-router-dom';

const ProtectRoute = ({ children }) => {
  const isLogin = localStorage.getItem('isLogin') === 'true';

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

  return <>{children}</>;
};

ProtectRoute 是一个包装组件 :它接收 children 作为需要保护的页面内容。检查登录状态后,未登录时渲染 <Navigate> 组件进行重定向,已登录时渲染 children

使用方式:

jsx 复制代码
<Route path="/pay" element={
  <ProtectRoute>
    <Pay />
  </ProtectRoute>
} />

Navigate 组件渲染时,React Router 会立即将 URL 重定向到 /login,并携带 state 对象,其中 from 记录了用户原本想访问的路径。登录组件通过 useLocation().state?.from 取回这个路径,登录成功后 navigate(from, { replace: true }) 跳转回去。

这个守卫模式的核心思路是:路由守卫不拦截路由匹配,而是在组件渲染层面做条件判断/pay 的路由始终匹配,但渲染的内容由 ProtectRoute 根据鉴权状态决定------要么是 Pay 组件,要么是重定向。

路由懒加载:React.lazy + Suspense

随着项目增长,把所有页面组件打包进一个 JS 文件会导致首屏加载缓慢。React Router 配合 React 的 lazySuspense 可以实现按路由拆分代码:

jsx 复制代码
import { lazy, Suspense } from 'react';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/Products/Detail'));
const NewProduct = lazy(() => import('./pages/Products/New'));
const Login = lazy(() => import('./pages/Login'));
const Pay = lazy(() => import('./pages/Pay'));
const NotFound = lazy(() => import('./pages/NotFound'));
const ProtectRoute = lazy(() => import('./ProtectRoute'));

lazy(() => import(...)) 是动态导入,Vite 会将其识别为代码分割点,为每个页面生成独立的 chunk。用户访问 /about 时,浏览器只加载 About 对应的 chunk,而不是整个应用。

Suspense 提供加载中的 fallback UI:

jsx 复制代码
<Router>
  <Suspense fallback={<div>loading...</div>}>
    <Navigation />
    <Routes>
      {/* ... */}
    </Routes>
  </Suspense>
</Router>

在懒加载组件还未下载完成时,Suspensefallback 会显示。下载完成后自动替换为真实组件。

重定向与 404 兜底

项目中有两种重定向场景:

静态重定向------旧路径永久迁移到新路径:

jsx 复制代码
<Route path="old-path" element={<Navigate replace to="new-path" />} />

用户访问 /old-path 时,Navigate 组件渲染,URL 立即变为 /new-pathreplace 确保旧路径不会留在历史记录中。

404 兜底------匹配所有未定义的路径:

jsx 复制代码
<Route path="*" element={<NotFound />} />

* 是通配符,匹配所有未被前面 Route 匹配的路径。它必须放在 Routes最后 ,因为 Routes 按顺序匹配,找到第一个匹配的 Route 就停止。如果 * 放在最前面,所有路径都会被它吞掉。

总结:React Router 全场景能力地图

能力 核心 API 使用场景
路由容器 BrowserRouter / HashRouter 顶层包裹,选择 History 或 Hash 模式
路由配置 Routes + Route 声明式配置路径与组件的映射
导航组件 Link 替代 <a> 标签,实现无刷新跳转
动态参数 useParams 从 URL 中提取 :id 等动态段
嵌套路由 Outlet 父路由提供布局,子路由渲染到插槽
编程式导航 useNavigate 表单提交后跳转、定时跳转
路由信息 useLocation 获取当前路径、state 传递的额外数据
路由守卫 自定义包装组件 + Navigate 鉴权拦截,未登录重定向到登录页
重定向 Navigate 组件 旧路径迁移、登录后跳回
懒加载 React.lazy + Suspense 按路由拆分代码,减少首屏加载
404 处理 <Route path="*"> 匹配所有未定义路径,兜底展示

从手写 HashRouter 理解 hashchange 事件,到手写 HistoryRouter 理解 pushStatepopstate,再到 React Router 将这些底层机制封装为声明式 API------前两篇文章建立的底层认知,是理解 React Router 每行配置背后"发生了什么"的关键。当你看到 Link 组件,你知道它底层调用了 history.pushState;当你看到 Navigate 组件,你知道它本质上是条件渲染 + replaceState;当你看到 ProtectRoute,你知道它不过是利用组件化特性实现的一个高阶守卫模式。

React Router v7 的 API 设计将"路由即组件"的理念贯彻到底------路由配置是 JSX,路由守卫是组件,重定向是组件,连懒加载都是 React 原生能力。这种一致性让 React Router 的学习曲线平滑,但只有理解底层原理,才能在遇到问题时知道从哪个方向排查。毕竟,所有前端路由框架的底层,最终都收敛到 hashchangepushStatereplaceStatepopstate 这四个 API 上。

相关推荐
用户938515635073 小时前
从零在浏览器里跑 DeepSeek-R1:WebGPU + Transformer.js 全链路实战(二)
前端·javascript·typescript
雪隐4 小时前
个人电脑玩AI-16让5060 Ti给你打工——MiniMax H3 提速实录:从"泡杯茶等视频"到"视频等你",一张 5060 Ti 的自我修养
前端·人工智能·后端
FungLeo4 小时前
Flutter Web 中文字体困境与渲染器选择:CanvasKit vs HTML renderer 实战
前端·flutter·html
敲代码的玉米C4 小时前
我修的那个 bug,制造了另一个 bug
前端·人工智能·架构
gis开发之家4 小时前
《Vue3 从入门到大神50篇》Vue3 源码详解(二十):生命周期钩子源码解析 —— onMounted / onUpdated 如何实现?
前端·javascript·前端框架·vue3·vue3源码
Narrastory4 小时前
我用 Claude Code 写代码不到 2 小时,却花了 3 天做完这个软件—Vibe Coding 的正确姿势,是设计不是生成
前端·人工智能·github
Enaium5 小时前
我实现了KMP的SDL绑定并编写了高性能软光追
前端·kotlin
古夕5 小时前
本地正常线上白屏:一次路由切换后刷新恢复问题的排查与修复
前端·ai编程
栀鸢ouo5 小时前
解决Element Plus表格展开行横向溢出、滚动截断问题(项目实战方案)
前端·vue.js