React: React Router 入门

最后更新:2026-08-26

Tom 正在开发一个电商管理后台,最初他用 useState 来切换"页面"——首页、商品列表、订单详情各一个 state 变量。随着页面增加到 10 个,状态逻辑乱成一团,URL 地址栏永远停留在 /,无法直接分享某个页面的链接。他意识到:缺少一个专业的路由方案


1. 你将学到



2. 概念图解

100%
flowchart LR
    A[BrowserRouter<br/>路由容器] --> B[Routes<br/>路由表]
    B --> C["Route path='/'<br/>→ Home"]
    B --> D["Route path='/products'<br/>→ ProductList"]
    B --> E["Route path='/products/:id'<br/>→ ProductDetail"]
    B --> F["Route path='*'<br/>→ NotFound"]
    C --> G[渲染组件]
    D --> G
    E --> G
    F --> G
    style A fill:#e1f5fe,stroke:#0288d1
    style B fill:#fff3e0,stroke:#f57c00
    style G fill:#e8f5e9,stroke:#388e3c

用户访问不同 URL → BrowserRouter 捕获 → Routes 匹配最合适的 Route → 渲染对应的组件。



3. 一个真实场景

Tom 的后台需要三个主页面:仪表盘、商品管理和系统设置。此外商品管理下还有商品列表和商品详情两个子页面。他希望每个页面都有独一无二的 URL,用户能通过浏览器的前进/后退按钮导航。

(1) 路由模式的选择

React Router 提供两种路由模式,它们的根本区别在于 URL 的处理方式:

模式 URL 示例 原理 适用场景
BrowserRouter example.com/users 利用 History API 操作 URL 服务器能配置 URL 重写的项目
HashRouter example.com/#/users 利用 URL hash 变化不触发服务器请求 静态托管(GitHub Pages、CDN)

选择建议: 能配服务器的项目一律用 BrowserRouter——URL 更干净、SEO 更友好。静态托管用 HashRouter

▶ 示例 1:基础路由配置

JSX
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'

function Home() {
  return <h2>仪表盘主页</h2>
}

function ProductList() {
  return <h2>商品列表</h2>
}

function Settings() {
  return <h2>系统设置</h2>
}

function NotFound() {
  return <h2>404 — 页面未找到</h2>
}

function App() {
  return (
    <BrowserRouter>
      <nav style={{ display: 'flex', gap: '1rem', padding: '1rem', background: '#f0f0f0' }}>
        <Link to="/">首页</Link>
        <Link to="/products">商品管理</Link>
        <Link to="/settings">系统设置</Link>
      </nav>

      <main style={{ padding: '1rem' }}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/products" element={<ProductList />} />
          <Route path="/settings" element={<Settings />} />
          <Route path="*" element={<NotFound />} />
        </Routes>
      </main>
    </BrowserRouter>
  )
}

export default App
▶ 试一试

执行效果: 点击导航链接,URL 随之变化,页面内容切换,但浏览器不会整页刷新——这就是 SPA 路由的核心体验。

关于 path="*" 通配符 * 匹配所有未在前面的 Route 中定义的路径,通常放在 Route 列表的末尾以实现 404 页面。在 React Router v6 中,* 只能作为 path 的末尾字符出现。

导航组件 用途 激活样式 适用场景
<Link to="/path"> 声明式跳转 通用导航链接
<NavLink to="/path"> 带激活状态的导航 isActive 回调 侧边栏/顶部导航高亮
navigate('/path') 编程式跳转 登录后跳转、表单提交后跳转
<Navigate to="/path" /> 声明式重定向 条件重定向组件

Tom 想在导航栏中高亮当前所在页面,让用户清楚地知道"我在哪里"。<NavLink> 组件提供了 isActive 参数,可以基于当前路由动态设置样式。它比 <Link> 多了两个额外属性:styleclassName 都支持接收一个带有 isActiveisPending 属性的回调函数。

▶ 示例 2:带激活样式的导航栏

JSX 📖 仅展示
import { NavLink } from 'react-router-dom'

function NavBar() {
  const linkStyle = {
    padding: '8px 16px',
    textDecoration: 'none',
    borderRadius: '6px',
    transition: 'all 0.2s'
  }

  const activeStyle = {
    ...linkStyle,
    backgroundColor: '#1976d2',
    color: '#fff',
    fontWeight: 'bold'
  }

  const inactiveStyle = {
    ...linkStyle,
    color: '#333'
  }

  return (
    <nav style={{ display: 'flex', gap: '12px', padding: '12px', background: '#fafafa' }}>
      <NavLink
        to="/"
        style={({ isActive }) => (isActive ? activeStyle : inactiveStyle)}
        end  // 精确匹配,避免 "/" 匹配所有以 "/" 开头的路径
      >
        首页
      </NavLink>
      <NavLink
        to="/products"
        style={({ isActive }) => (isActive ? activeStyle : inactiveStyle)}
      >
        商品管理
      </NavLink>
      <NavLink
        to="/settings"
        style={({ isActive }) => (isActive ? activeStyle : inactiveStyle)}
      >
        系统设置
      </NavLink>
    </nav>
  )
}
逻辑代码 42 行(超过 40 行限制,仅展示)

关键点: end 属性让 / 路径只在完全匹配时激活,否则所有路径都会命中首页的激活样式。NavLinkclassName 也支持回调函数,适合使用 CSS 类名的项目。

(3) 嵌套路由与布局共享

Tom 发现商品管理页面内部还有"商品列表"和"添加商品"两个子页面,它们共享同一个侧边栏布局。如果每个子页面都重复写布局代码,既冗余又难以维护。React Router 的嵌套路由 + <Outlet> 完美解决这个问题。

嵌套路由的核心思想是父路由定义布局骨架,子路由通过 Outlet 注入内容。父组件不关心子路由具体渲染什么,只负责布局框架。

▶ 示例 3:嵌套路由与 Outlet

JSX 📖 仅展示
import { BrowserRouter, Routes, Route, Link, Outlet, useParams } from 'react-router-dom'

// 父布局组件 — 共享的侧边栏 + Outlet
function ProductsLayout() {
  return (
    <div style={{ display: 'flex' }}>
      <aside style={{ width: '200px', padding: '16px', background: '#f5f5f5' }}>
        <h3>商品管理</h3>
        <nav style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
          <Link to="list">商品列表</Link>
          <Link to="add">添加商品</Link>
        </nav>
      </aside>
      <main style={{ flex: 1, padding: '16px' }}>
        {/* 子路由的组件在这里渲染 */}
        <Outlet />
      </main>
    </div>
  )
}

function ProductList() {
  const products = [
    { id: 1, name: 'React 编程书', price: 79 },
    { id: 2, name: 'TypeScript 指南', price: 59 },
    { id: 3, name: 'Node.js 实战', price: 69 }
  ]

  return (
    <div>
      <h2>商品列表</h2>
      <ul>
        {products.map(p => (
          <li key={p.id}>
            <Link to={`/products/detail/${p.id}`}>
              {p.name} — ${p.price}
            </Link>
          </li>
        ))}
      </ul>
    </div>
  )
}

function AddProduct() {
  return (
    <div>
      <h2>添加商品</h2>
      <form onSubmit={e => { e.preventDefault(); alert('提交成功!') }}>
        <div><label>商品名:<input name="name" /></label></div>
        <div><label>价格:<input name="price" type="number" /></label></div>
        <button type="submit">提交</button>
      </form>
    </div>
  )
}

function ProductDetail() {
  const { id } = useParams()
  return <h2>商品详情(ID:{id})</h2>
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<h2>首页</h2>} />
        {/* 嵌套路由:父路由带布局,子路由通过 Outlet 渲染 */}
        <Route path="/products" element={<ProductsLayout />}>
          <Route index element={<ProductList />} />          {/* /products 默认显示 */}
          <Route path="list" element={<ProductList />} />     {/* /products/list */}
          <Route path="add" element={<AddProduct />} />       {/* /products/add */}
          <Route path="detail/:id" element={<ProductDetail />} /> {/* /products/detail/1 */}
        </Route>
        <Route path="*" element={<h2>404 未找到</h2>} />
      </Routes>
    </BrowserRouter>
  )
}
逻辑代码 72 行(超过 40 行限制,仅展示)

运行逻辑: 访问 /products/listProductsLayout 渲染侧边栏 → Outlet 位置显示 ProductList 组件。URL 结构与组件结构一一对应,清晰可维护。

关于 index 路由: index 路由是父路径的默认子路由。当访问 /products 时,没有匹配到任何子路径(listadd 都不匹配),此时 index 路由的内容会出现在 Outlet 中。它确保父路径不会显示空白区域。



4. 动态路由与路径参数

路径模式 URL 示例 useParams 返回值 说明
/users/:id /users/42 { id: '42' } 单个动态参数
/users/:userId/posts/:postId /users/42/posts/99 { userId: '42', postId: '99' } 多段动态参数
/files/* /files/a/b/c { '*': 'a/b/c' } 通配符匹配剩余路径
/categories/:catId/:tab? 需定义两个 Route { catId, tab } 可选参数(无原生支持)

动态路由是路由系统中最重要的功能之一。Tom 的商品详情页需要根据不同的商品 ID 展示不同的内容,他不可能为每个商品 ID 都写一个 Route——这就需要用 :param 语法定义动态路径。

(1) useParams 的使用

:id 是动态参数的占位符,实际的取值通过 useParams 这个 Hook 从 URL 中提取。

JSX
function UserDetail() {
  const { userId, postId } = useParams()
  return <p>用户 {userId} 的文章 {postId}</p>
}
// URL: /users/42/posts/99 → userId=42, postId=99
▶ 试一试

▶ 示例 4:商品详情页

JSX
import { useParams, Link, useNavigate } from 'react-router-dom'

// 模拟商品数据
const products = [
  { id: '1', name: 'React 编程书', price: 79, description: '从零到一掌握 React 18 开发' },
  { id: '2', name: 'TypeScript 指南', price: 59, description: '系统学习 TypeScript 类型系统' },
  { id: '3', name: 'Node.js 实战', price: 69, description: '后端开发从入门到进阶' }
]

function ProductDetail() {
  const { id } = useParams()
  const navigate = useNavigate()
  const product = products.find(p => p.id === id)

  if (!product) {
    return (
      <div>
        <h2>商品不存在</h2>
        <button onClick={() => navigate('/products')}>返回商品列表</button>
      </div>
    )
  }

  return (
    <div>
      <h2>{product.name}</h2>
      <p className="price">价格:${product.price}</p>
      <p className="desc">{product.description}</p>
      <Link to="/products">← 返回列表</Link>
    </div>
  )
}
▶ 试一试

注意事项: 使用动态参数时需要考虑"参数值不存在对应数据"的情况。上述代码中的 if (!product) 分支就是空数据处理,避免访问不存在的 ID 时页面白屏。

(2) 多段动态参数

一个路径中可以包含多个动态参数,常见于嵌套资源的场景:

JSX
// 路由定义
<Route path="/categories/:catId/products/:prodId" element={<ProductView />} />

// 组件中提取
function ProductView() {
  const { catId, prodId } = useParams()
  // URL: /categories/electronics/products/42
  // catId = "electronics", prodId = "42"
  return <h2>分类 {catId} 下的商品 {prodId}</h2>
}
▶ 试一试

(3) 可选参数与通配符

React Router v6 不直接支持可选参数,但可以通过两种方式实现类似效果:

JSX
// 方式一:定义两个 Route(推荐)
<Route path="/categories/:catId" element={<CategoryPage />} />
<Route path="/categories/:catId/:tab" element={<CategoryPage />} />

// 方式二:在组件内自行判断
function CategoryPage() {
  const { catId, tab } = useParams()
  const activeTab = tab || 'overview'  // 默认值
  return <h2>分类 {catId} - {activeTab}</h2>
}
▶ 试一试

选择方式一的好处是 URL 语义清晰,方式二更简洁但降低了 URL 的可读性。

(4) 路径匹配优先级详解

React Router v6 的路径匹配基于评分算法,而非传统框架的"先匹配先服务"。理解这套规则有助于排查路由不生效的问题。

JSX
// 假设有以下路由配置
<Routes>
  <Route path="/products/new" element={<NewProduct />} />     {/* 静态路径 */}
  <Route path="/products/:id" element={<ProductDetail />} />  {/* 动态路径 */}
  <Route path="/products/:id/edit" element={<EditProduct />} /> {/* 混合路径 */}
</Routes>
▶ 试一试

匹配优先级(从高到低):

  1. 静态路径段new)优先于动态参数段:id
  2. 更多静态段的路径优先于更少静态段的路径
  3. 路径段数多的优先于段数少的

所以访问 /products/new 匹配 <NewProduct />,访问 /products/42 匹配 <ProductDetail />,访问 /products/42/edit 匹配 <EditProduct />。开发者无需担心顺序——系统会自动选择"最佳匹配"。


❓ 常见问题

Q BrowserRouter 部署到服务器为什么刷新就 404?
A 因为浏览器向服务器请求 /products/detail/1 这个具体路径,但服务器上没有这个文件。解决方案:在 nginx 中配置 try_files $uri $uri/ /index.html,将所有路由请求都指向 index.html,由 React Router 在前端做匹配。如果无法配置服务器,改用 HashRouter(# 后的部分不会发送到服务器)。
Q <Link> 和原生 <a> 标签有什么区别?
A <a> 点击后会触发浏览器整页刷新,导致 SPA 丢失所有内存状态。<Link> 会阻止默认跳转,通过 History API 更新 URL 并通知 React Router 渲染新组件——不刷新页面、不丢失状态。SPA 中始终用 Link 或 NavLink 代替 <a>
Q Route 的 path 匹配规则是怎样的?多个 Route 顺序重要吗?
A React Router v6 使用自动排名匹配(而非顺序匹配)。系统会根据 path 的"具体程度"自动打分:/users/new/users/:id 更具体,因此优先匹配。* 通配符匹配所有未匹配路径,通常放在最后用作 404 页面。路径默认是前缀匹配,添加 end 属性可改为精确匹配。
Q index 路由的作用是什么?
A <Route index element={...} /> 定义父路由的默认子路由。当访问父路由本身的路径时(如 /products),如果父路由使用了 Outlet,index 路由的内容会显示在 Outlet 位置。它相当于"父路径下的默认页",避免访问父路径时出现空白区域。
Q React Router v6 和 v5 有什么主要区别?
A v6 三个重大变化:① 用 <Routes> + <Route> 替代 <Switch>,Route 组件自动匹配最具体的路径;② 嵌套路由用 <Outlet> 替代手动渲染子路由;③ useNavigate() 替代 useHistory()——navigate('/path') 替代 history.push('/path')navigate(-1) 替代 history.goBack()。v6 的 API 更简洁,但迁移需要改不少代码。

(5) 相对路径 vs 绝对路径

在嵌套路由中,<Link to="..."> 的路径是相对于当前路由的,而 <Link to="/..."> 是绝对路径。理解这个区别对避免导航异常至关重要。

JSX
// 当前在 /products 下(ProductsLayout 组件内)

<Link to="list">        {/* → /products/list(相对路径,拼接到当前路由后) */}
<Link to="/list">       {/* → /list(绝对路径,直接替换) */}
<Link to="../settings"> {/* → /settings(父级相对路径) */}
▶ 试一试

在嵌套路由中,如果子路由在 ProductsLayout 中,内部的所有 Link 应该使用相对路径(不加前导 /),这样当父路由路径变化时,子路由的 Link 自动适配。



5. 404 页面与路由设计模式

(1) 通配符路由

path="*" 匹配所有未定义路径,是实现 404 页面的标准方式。但在 React Router v6 中,* 只能出现在路径末尾,不能像 path="/users/*/edit" 这样使用。

JSX
function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/products" element={<ProductList />} />
      <Route path="/products/:id" element={<ProductDetail />} />
      <Route path="/about" element={<About />} />

      {/* 通配符路由必须放在最后 */}
      <Route path="*" element={<NotFound />} />
    </Routes>
  )
}

function NotFound() {
  return (
    <div style={{ textAlign: 'center', padding: '40px' }}>
      <h1>404</h1>
      <p>抱歉,您访问的页面不存在。</p>
      <Link to="/">返回首页</Link>
    </div>
  )
}
▶ 试一试

(2) useNavigate 基础用法

虽然 useNavigate 在进阶课程中会详细讲解,但 Tom 在基础阶段也需要在某些场景下使用——例如登录页倒计时跳转、表单提交后跳转。

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

function OrderSuccess() {
  const navigate = useNavigate()
  const [countdown, setCountdown] = useState(5)

  useEffect(() => {
    const timer = setInterval(() => {
      setCountdown(prev => {
        if (prev <= 1) {
          clearInterval(timer)
          navigate('/orders')  // 倒计时结束自动跳转
          return 0
        }
        return prev - 1
      })
    }, 1000)

    return () => clearInterval(timer)
  }, [navigate])

  return (
    <div>
      <h2>下单成功!</h2>
      <p>{countdown} 秒后自动跳转到订单页</p>
      <button onClick={() => navigate('/orders')}>立即查看</button>
    </div>
  )
}
▶ 试一试

▶ 示例 5:组合所有功能的管理后台

将本课学到的所有概念整合在一起——BrowserRouter、Routes、嵌套路由、NavLink、useParams、404 页面——构建一个完整的管理后台骨架。

JSX 📖 仅展示
import { BrowserRouter, Routes, Route, NavLink, Outlet, useParams } from 'react-router-dom'
import './App.css'

// 布局组件
function AdminLayout() {
  return (
    <div className="admin-container">
      <header className="admin-header">
        <h1>Tom 电商管理后台</h1>
      </header>
      <div className="admin-body">
        <nav className="admin-sidebar">
          <NavLink to="/" end>仪表盘</NavLink>
          <NavLink to="/products">商品管理</NavLink>
          <NavLink to="/orders">订单管理</NavLink>
          <NavLink to="/settings">系统设置</NavLink>
        </nav>
        <main className="admin-content">
          <Outlet />
        </main>
      </div>
    </div>
  )
}

// 页面组件
function Dashboard() {
  return <h2>欢迎回来,Tom!今日订单数:42</h2>
}

function ProductsLayout() {
  return (
    <div>
      <h2>商品管理</h2>
      <nav>
        <NavLink to="list">商品列表</NavLink>
        <NavLink to="add">添加商品</NavLink>
      </nav>
      <Outlet />
    </div>
  )
}

function ProductList() {
  return <p>这里显示商品列表...</p>
}

function AddProduct() {
  return <p>这里显示添加商品表单...</p>
}

function Orders() {
  return <h2>订单管理</h2>
}

function Settings() {
  return <h2>系统设置</h2>
}

function NotFound() {
  return <h2>404 — 页面不存在</h2>
}

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<AdminLayout />}>
          <Route index element={<Dashboard />} />
          <Route path="products" element={<ProductsLayout />}>
            <Route index element={<ProductList />} />
            <Route path="list" element={<ProductList />} />
            <Route path="add" element={<AddProduct />} />
          </Route>
          <Route path="orders" element={<Orders />} />
          <Route path="settings" element={<Settings />} />
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>
  )
}
逻辑代码 71 行(超过 40 行限制,仅展示)

架构特点: 整个应用只有一个 <BrowserRouter>,一个顶级 <Routes>AdminLayout 作为全局布局通过 Outlet 嵌套所有页面组件。这种"单路由 + 布局嵌套"的模式是中型 React 应用的标准架构。


📖 小节


📝 作业

  1. 创建一个 4 页面的管理后台:首页、用户列表、用户详情(通过 useParams 读取用户 ID)、关于页,使用 Link 进行导航,并确保 NavLink 有激活样式。
  2. 为用户列表页添加嵌套路由:/users 显示用户列表,/users/:id 显示用户详情,两者共享一个带标题的布局组件(使用 Outlet)。
  3. 在路由表末尾添加一个 404 页面,当用户访问不存在的路径时显示友好的提示信息。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏