React Router v7 完全实战指南:从入门到鉴权,一套代码全搞定

前言

上一篇《从多页到 SPA:手写一个 Hash 路由》我们手写了 Hash 路由,理解了前端路由的本质。但实际项目中,我们不会每次都从零造轮子------React Router 就是 React 生态中最主流的前端路由方案。

本文基于 React Router v7(最新版本),通过一个完整的 Demo 项目,覆盖从基础配置、动态路由、嵌套路由、懒加载,到鉴权守卫、404 兜底等所有实际项目中的高频场景。


一、项目起步

技术栈

工具 版本 作用
Vite ^8.1.1 构建工具
React ^19.2.7 UI 框架
react-router-dom ^7.18.2 前端路由

入口文件

jsx 复制代码
// src/main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

入口一如既往地简洁------因为路由逻辑全部封装在 App.jsx 中。


二、HashRouter vs BrowserRouter:两种路由模式

在正式开始之前,必须先搞清楚一个重要的选型:

bash 复制代码
HashRouter:     http://localhost:5173/#/pay
BrowserRouter:  http://localhost:5173/pay
HashRouter BrowserRouter
URL 外观 #,有点"丑" 干净,和传统 URL 一样
原理 监听 hashchange 事件 基于 HTML5 History API(pushState/popState
服务器支持 不需要 需要服务器配置(所有路径返回 index.html
SEO 差(hash 不会被搜索引擎收录)
适用场景 简单 demo、不需要 SEO 的后台系统 绝大多数生产项目

本文 Demo 使用 BrowserRouter,这是现代 SPA 的主流选择。


三、基础路由配置:Routes + Route

React Router v7 的核心配置模式是 组件式配置------路由就是组件,配置即声明。

jsx 复制代码
// src/App.jsx(核心骨架)
import {
  BrowserRouter as Router,
  Routes,
  Route,
} from 'react-router-dom'

const App = () => {
  return (
    <Router>
      <Navigation />
      <div id="container">
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="*" element={<NotFound />} />
        </Routes>
      </div>
    </Router>
  )
}

关键点

  • <Router> :最外层包裹,整个应用的路由容器(这里用了 BrowserRouter
  • <Routes> :路由匹配区域,有且只有一个 <Route> 会被渲染
  • <Route> :单条路由规则,path 匹配 URL,element 指定渲染的组件
  • path="*":通配符,匹配所有未命中路径,用于 404 兜底

注意<Navigation /> 放在 <Routes> 外面,意味着它在所有页面都显示------这就是 SPA 的"公共区域"(页头/导航栏)。


传统的 <a href="/about"> 会触发浏览器完整刷新,这在 SPA 中是灾难性的。

React Router 提供了 <Link> 组件------它在底层拦截了点击事件,只更新 URL 和渲染内容,不刷新页面

jsx 复制代码
// src/components/Navigation.jsx
import { Link } from 'react-router-dom';

function Navigation() {
  return (
    <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>
        <li><Link to="/products/new">商品新增</Link></li>
        <li><Link to="/pay">支付</Link></li>
      </ul>
    </nav>
  );
}

<Link> 本质上渲染的还是 <a> 标签,但它接管了点击行为,调用 history.pushState 来改变 URL。


五、路由懒加载:让首页飞起来

如果所有页面组件都在首页一次性加载,JS bundle 会非常大,首页加载速度堪忧。

React.lazy + Suspense 实现了"按需加载"------只有访问某个路由时,才去下载对应组件的代码。

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

// ❌ 传统方式:首页就全部加载
// import Home from './pages/Home';
// import About from './pages/About';

// ✅ 懒加载:访问时才下载
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
const NotFound = lazy(() => import('./pages/NotFound'));

const App = () => {
  return (
    <Router>
      {/* Suspense 是懒加载的"加载中"兜底 */}
      <Suspense fallback={<div>Loading...</div>}>
        <Navigation />
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          {/* ... */}
        </Routes>
      </Suspense>
    </Router>
  )
}

执行流程

xml 复制代码
用户访问 /about
  → about 的 JS chunk 还没下载
  → 显示 <Suspense fallback>(Loading...)
  → 异步下载完成
  → 渲染 <About />

打开浏览器 DevTools 的 Network 面板,你会看到每个页面都是独立的 JS 文件,只在首次访问时加载。


六、动态路由:/user/:id 的参数魔法

同一个页面模板,根据 URL 中的不同参数显示不同内容------这就是动态路由。

jsx 复制代码
{/* 配置 */}
<Route path="/user/:id" element={<UserProfile />} />
jsx 复制代码
// src/pages/UserProfile/index.jsx
import { useParams } from 'react-router-dom';

function UserProfile() {
  let { id } = useParams(); // 取出 URL 参数

  return (
    <h2>User Profile: {id}</h2>
  )
}
访问 URL useParams() 结果
/user/123 { id: "123" }
/user/小家 { id: "小家" }

useParams 是 React Router 最常用的 Hook 之一------召之即来,来之即用


七、嵌套路由:父子页面的优雅组织

实际项目中,页面往往有层级关系。比如 /products 是商品列表,/products/123 是商品详情,/products/new 是新增商品。

jsx 复制代码
{/* 嵌套路由配置 */}
<Route path="/products" element={<Products />}>
  <Route path=":productId" element={<ProductDetail />} />
  <Route path="new" element={<NewProduct />} />
</Route>

注意:子路由的 path相对路径 ,不需要写 /products/:productId

jsx 复制代码
// src/pages/Products/index.jsx
import { Outlet } from 'react-router-dom';

const Products = () => {
  return (
    <>
      <h1>产品列表</h1>
      {/* Outlet 是子路由的"插槽",子组件渲染在这里 */}
      <Outlet />
    </>
  )
}

渲染结果

bash 复制代码
访问 /products          →  产品列表(Outlet 为空)
访问 /products/123      →  产品列表 + 产品详情 123
访问 /products/new      →  产品列表 + 新增商品

<Outlet /> 就是嵌套路由的"占位符"------父组件决定公共布局,子组件填充可变区域。


八、重定向:Navigate 组件

项目迭代中,有些旧 URL 需要跳转到新地址;或者未登录用户访问需要跳转登录页。

jsx 复制代码
{/* 重定向:/old-path → /new-path */}
<Route path="/old-path" element={
  <Navigate replace to="/new-path" />
} />
属性 作用
to 目标路径
replace true 时用新记录替换当前历史记录(用户无法"后退"回来)

replace 在登录场景尤其重要------登录成功后用户不应该还能"后退"回登录页。


九、鉴权路由:ProtectRoute 路由守卫

这是企业项目中最常见的需求------某些页面需要登录才能访问

jsx 复制代码
// src/App.jsx
<Route path="/pay" element={
  <ProtectRoute>
    <Pay />
  </ProtectRoute>
} />
jsx 复制代码
// src/ProtectRoute.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}</>;
}

核心设计模式:children 插槽

xml 复制代码
<ProtectRoute>
  <Pay />          ← 这个就是 children
</ProtectRoute>
jsx 复制代码
const ProtectRoute = ({ children }) => {
  // children 就是 <Pay />
  // 已验证:渲染 {children}
  // 未验证:渲染 <Navigate>(不渲染 children)
}

这就是 React 组件化的精髓------children 让组件拥有了"包裹"其他组件的能力,类比弹窗的 Modal 组件:

jsx 复制代码
<Modal>                ← 蒙层 + 窗体框架
  <form>...</form>     ← children 定制内容
</Modal>

十、登录流程:useNavigate + useLocation + state

鉴权路由拦截后重定向到 /login,同时传递 state 记录来源路径------登录成功后自动跳回。

jsx 复制代码
// src/pages/Login/index.jsx
import { useNavigate, useLocation } from 'react-router-dom';

const Login = () => {
  const navigate = useNavigate();
  const location = useLocation();

  // 从 state 中取出"从哪来",没有则默认跳首页
  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');
      // replace: true --- 不让用户后退回登录页
      navigate(from, { replace: true });
    }
  }

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

登录流程全链路

bash 复制代码
1. 未登录用户访问 /pay
2. ProtectRoute 检测到 isLogin=false
3. <Navigate to="/login" state={{ from: "/pay" }} />
4. 用户在 /login 输入账号密码并提交
5. 登录成功,localStorage.setItem('isLogin', 'true')
6. navigate("/pay", { replace: true })
   --- 跳回 /pay,并且 /login 不会留在浏览器历史中

?. 是 ES11 的可选链运算符location.state?.from 安全地读取可能不存在的嵌套属性,避免了 Cannot read property 'from' of undefined


十一、404 页面:通配符路由的兜底艺术

jsx 复制代码
{/* * 放在最后,兜底所有未匹配的路径 */}
<Route path="*" element={<NotFound />} />
jsx 复制代码
// src/pages/NotFound/index.jsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

const NotFound = () => {
  let navigate = useNavigate();

  useEffect(() => {
    // 3 秒后自动跳回首页
    setTimeout(() => navigate('/'), 3000);
  }, []);

  return <h1>Not Found</h1>;
}

注意顺序path="*" 必须放在 <Routes>最后 。React Router 按顺序匹配,* 在前面会吞掉所有路由。


十二、完整路由架构一览

把以上所有知识点串起来,就是这个项目的完整路由树:

bash 复制代码
<Router>                          // 路由容器
  <Suspense fallback={Loading}>   // 懒加载兜底
    <Navigation />                // 全局导航(不受路由切换影响)
    <Routes>                      // 路由匹配区
      /                → Home
      /about           → About
      /user/:id         → UserProfile        (动态路由)
      /products         → Products + Outlet  (嵌套路由)
        /products/:productId → Detail
        /products/new        → NewProduct
      /old-path         → Navigate → /new-path (重定向)
      /login            → Login              (登录页)
      /pay              → ProtectRoute → Pay  (鉴权守卫)
      *                 → NotFound           (404 兜底)
    </Routes>
  </Suspense>
</Router>

十三、总结:一张图理解 React Router

ini 复制代码
┌─────────────────────────────────────────────────┐
│                    <Router>                      │
│  ┌──────────┐  ┌──────────────────────────────┐ │
│  │Navigation│  │         <Routes>              │ │
│  │ (始终显示)│  │  path="/"     → <Home />     │ │
│  │          │  │  path="/about"→ <About />    │ │
│  │ <Link>   │  │  path="/user/:id" → 动态路由  │ │
│  │ <Link>   │  │  path="/products" → 嵌套+Outlet│ │
│  │ <Link>   │  │  path="/pay" → 鉴权守卫       │ │
│  │          │  │  path="*"     → 404 兜底      │ │
│  └──────────┘  └──────────────────────────────┘ │
└─────────────────────────────────────────────────┘
概念 一句话总结
BrowserRouter 基于 History API,URL 干净,生产项目首选
Routes + Route 组件式配置,声明即路由
Link SPA 的 <a> 标签替代品,不刷新页面
lazy + Suspense 按需加载页面代码,优化首屏速度
useParams 从 URL 中取出动态参数 :id
Outlet 嵌套路由的子组件"插槽"
Navigate 声明式重定向
useNavigate 命令式跳转(事件处理中调用)
useLocation 获取当前 URL 和路由 state
ProtectRoute children 模式实现鉴权守卫
path="*" 通配符兜底 404

下一步

如果你是从上一篇 Hash 路由文章过来的,现在应该能清晰地看到技术演进的脉络:

bash 复制代码
原生 hashchange 事件
    ↓
手写 HashRouter 类
    ↓
React Router(hash 模式)
    ↓
React Router(history 模式)+ 懒加载 + 鉴权 + 嵌套路由

下一步可以深入学习:

  1. React Router 源码:它的路由匹配算法是怎么实现的?
  2. SSR 路由:Next.js 的文件系统路由
  3. 状态管理联动:zustand + React Router 的配合

如果这篇文章对你有帮助,欢迎点赞、收藏、评论🎉 你的支持是我持续输出的动力!


相关推荐
To_OC1 小时前
LC 74 搜索二维矩阵:换皮的二分查找,我居然一开始没看出来
javascript·算法·leetcode
橘子星1 小时前
一篇文章搞懂 useRef:聚焦 DOM、模拟 forceRender、管理 Worker 都靠它
前端·javascript
前端Hardy1 小时前
Vue 终于杀进终端界!这个开源项目让 CLI 开发像写网页一样简单
前端·javascript·后端
一次旅行1 小时前
多智能体编排实战:拆解Plan-and-Execute范式+三层记忆架构,手写无依赖轻量Agent调度引擎
前端·javascript·架构
sugar__salt4 小时前
跟着 Demo 学 Pinia:两种仓库写法 + 完整 TodoList 复现
前端·javascript·vue.js·前端框架·vue
大家的林语冰5 小时前
🎉 Vercel 官宣 Next 16.3 正式发布,GitHub 第一全栈框架再次进化!
前端·javascript·前端框架
张元清7 小时前
React useEvent Hook:告别过期闭包的稳定回调 (2026)
javascript·react.js
用户938515635077 小时前
从 DOM 编程到声明式 UI:React useRef 与 useState 底层全解析
前端·javascript·react.js
小月土星7 小时前
19.LeetCode 删除链表的倒数第 N 个节点 超详细题解(JS 版)
javascript