「前端何去何从」React Router:让单页应用有多页的体验

React Router:让单页应用有多页的体验

URL 不只是地址栏里的文字,它是用户和应用之间的契约

开篇:为什么需要路由

在前几篇里,我们已经知道:

  • React 组件会根据 props 和 state 渲染 UI
  • 用户操作会触发事件,事件会更新状态
  • 状态变化会让 React 重新渲染

但学到这里,你会开始遇到一个新的问题:你的应用只有一个页面

什么意思?就是不管你点什么、做什么,URL 都是 http://localhost:3000/,不会变。

这会带来几个实际问题:

  • 用户想分享某个页面,但 URL 永远是同一个
  • 用户想用浏览器的后退按钮,但点了没反应
  • 用户想收藏某个页面,但收藏的是整个应用,不是具体内容
  • 搜索引擎不知道你的应用有哪些页面

这些问题的根源是:单页应用(SPA)只有一个 HTML 文件,所有内容都是 JavaScript 动态切换的

传统网站是"多页应用"------每个页面对应一个 HTML 文件,URL 自然就不同。

但单页应用把所有逻辑都放在一个文件里,URL 就"失联"了。

React Router 就是为了解决这个问题。它让 URL 和 UI 保持同步------URL 变了,UI 就跟着变;UI 变了,URL 也跟着变。

本文会覆盖什么

  • React Router 核心概念
  • 基础路由配置
  • 嵌套路由
  • 动态路由参数
  • 编程式导航
  • 404 页面处理
  • 实战案例

学完之后你应该能做到什么

如果你认真跟着本文走完,应该能掌握这些能力:

  • 能配置基础路由,让不同 URL 显示不同组件
  • 能用嵌套路由组织复杂的页面结构
  • 能用动态参数处理用户详情、文章详情这类页面
  • 能用编程式导航在代码里控制跳转
  • 能处理 404 页面和重定向

Part 1: 单页应用的路由问题

传统多页应用 vs 单页应用

先搞清楚两种应用模式的区别:

传统多页应用 单页应用(SPA)
页面加载 每次跳转都加载新 HTML 只加载一次,之后动态切换
URL 变化 浏览器自动处理 需要 JavaScript 处理
用户体验 有白屏,但 URL 总是对的 流畅,但 URL 可能"失联"
典型技术 PHP、JSP、传统前端 React、Vue、Angular

传统多页应用的 URL 是"真的"------每个 URL 对应一个真实的 HTML 文件。

浏览器会自动处理 URL 变化、后退按钮、收藏夹。

单页应用的 URL 是"假的"------只有一个 HTML 文件,所有内容都是 JavaScript 动态生成的。

浏览器不知道你"换了页面",它只知道你一直在同一个页面上。

URL 的重要性

URL 不只是地址栏里的文字,它是用户和应用之间的契约:

  • 分享:用户想把某个页面发给朋友,需要一个唯一的 URL
  • 后退:用户想回到上一个页面,需要浏览器的后退按钮能正常工作
  • 收藏:用户想收藏某个页面,需要一个稳定的 URL
  • SEO:搜索引擎需要 URL 来索引你的页面

如果 URL 不工作,用户会觉得你的应用"坏了"。

History API 简介

浏览器提供了 History API,让 JavaScript 可以"假装"在切换页面:

csharp 复制代码
// 添加一条历史记录,URL 变成 /about
history.pushState(null, '', '/about');

// 替换当前历史记录,URL 变成 /home
history.replaceState(null, '', '/home');

// 后退
history.back();

// 前进
history.forward();

这些 API 让 JavaScript 可以控制 URL,而不需要真的加载新页面。

React Router 如何解决这些问题

React Router 封装了 History API,提供了声明式的路由配置:

javascript 复制代码
// 声明:当 URL 是 /about 时,显示 About 组件
<Route path="/about" element={<About />} />

// 声明:点击这个链接,跳转到 /about
<Link to="/about">关于</Link>

你不需要手动调用 history.pushState,只需要声明"什么 URL 显示什么组件",React Router 会帮你处理剩下的事情。


Part 2: React Router 核心概念

BrowserRouter vs HashRouter

React Router 提供了两种路由器:

BrowserRouter HashRouter
URL 格式 /about /#/about
原理 使用 History API 使用 URL hash
服务器配置 需要配置 不需要
适合场景 有服务器控制权 静态托管(如 GitHub Pages)

BrowserRouter 是更常见的选择,URL 更干净。但它需要服务器配合------当用户直接访问 /about 时,服务器需要返回 index.html,而不是 404。

HashRouter 不需要服务器配合,因为 hash 部分不会发送给服务器。但 URL 里会有一个 #,看起来不太美观。

个人建议:如果你有服务器控制权,用 BrowserRouter;如果是纯静态托管(比如 GitHub Pages),用 HashRouter。

Route 组件:路径和组件的映射

Route 是 React Router 的核心,它声明了"什么 URL 显示什么组件":

ini 复制代码
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
  • path:URL 路径
  • element:要渲染的组件

Link 是 React Router 提供的导航组件,替代 HTML 的 <a> 标签:

ini 复制代码
<Link to="/about">关于</Link>
<Link to="/contact">联系我们</Link>
ini 复制代码
// ❌ 用 a 标签:会导致整页刷新
<a href="/about">关于</a>

// ✅ 用 Link:只切换组件,不刷新页面
<Link to="/about">关于</Link>

为什么不能用 <a> 标签?因为 <a> 会让浏览器重新加载整个页面,失去单页应用的流畅体验。Link 组件会阻止默认行为,只切换组件。


Part 3: 基础路由配置

安装和设置

复制代码
npm install react-router-dom

然后在应用的入口文件(通常是 main.jsxApp.jsx)配置路由:

javascript 复制代码
import { BrowserRouter, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Routes>
    </BrowserRouter>
  );
}

Routes 和 Route 的用法

Routes 是路由容器,Route 是路由规则:

xml 复制代码
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  <Route path="/contact" element={<Contact />} />
</Routes>

React Router 会根据当前 URL,找到匹配的 Route,渲染对应的组件。

完整的基础示例

javascript 复制代码
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

function Home() {
  return <h1>首页</h1>;
}

function About() {
  return <h1>关于我们</h1>;
}

function Contact() {
  return <h1>联系我们</h1>;
}

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">首页</Link>
        <Link to="/about">关于</Link>
        <Link to="/contact">联系我们</Link>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Routes>
    </BrowserRouter>
  );
}

这样,点击不同的链接,URL 会变,显示的组件也会变,但页面不会刷新。


Part 4: 嵌套路由

为什么需要嵌套路由

很多页面有"布局"------比如后台管理系统,通常有:

  • 一个顶部导航栏
  • 一个侧边栏
  • 一个主内容区

当用户切换页面时,顶部导航栏和侧边栏不变,只有主内容区变化。

如果不用嵌套路由,你可能需要在每个页面组件里都写一遍导航栏和侧边栏:

javascript 复制代码
// ❌ 每个页面都重复布局
function Dashboard() {
  return (
    <div>
      <Header />
      <Sidebar />
      <main>Dashboard 内容</main>
    </div>
  );
}

function Settings() {
  return (
    <div>
      <Header />
      <Sidebar />
      <main>设置页面</main>
    </div>
  );
}

这显然很蠢。嵌套路由让你只写一次布局:

javascript 复制代码
// ✅ 布局只写一次
function Layout() {
  return (
    <div>
      <Header />
      <Sidebar />
      <Outlet /> {/* 这里会渲染子路由 */}
    </div>
  );
}

Outlet 组件

Outlet 是 React Router 提供的"占位符",它会渲染匹配的子路由:

javascript 复制代码
import { Outlet } from 'react-router-dom';

function Layout() {
  return (
    <div>
      <Header />
      <Sidebar />
      <main>
        <Outlet />
      </main>
    </div>
  );
}

布局组件模式

把布局和路由结合起来:

xml 复制代码
<Routes>
  <Route path="/" element={<Layout />}>
    <Route index element={<Home />} />
    <Route path="about" element={<About />} />
    <Route path="contact" element={<Contact />} />
  </Route>
</Routes>

注意这里:

  • 父路由 path="/"Layout 作为布局
  • 子路由 index 表示默认页面
  • 子路由的 path 不需要写 /,它会自动拼接

实战:Dashboard 案例

javascript 复制代码
import { BrowserRouter, Routes, Route, Link, Outlet } from 'react-router-dom';

function DashboardLayout() {
  return (
    <div className="dashboard">
      <nav className="sidebar">
        <Link to="/dashboard">概览</Link>
        <Link to="/dashboard/users">用户管理</Link>
        <Link to="/dashboard/settings">设置</Link>
      </nav>
      <main className="content">
        <Outlet />
      </main>
    </div>
  );
}

function DashboardHome() {
  return <h2>Dashboard 概览</h2>;
}

function Users() {
  return <h2>用户管理</h2>;
}

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

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/dashboard" element={<DashboardLayout />}>
          <Route index element={<DashboardHome />} />
          <Route path="users" element={<Users />} />
          <Route path="settings" element={<Settings />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

这样,切换页面时,侧边栏不会重新渲染,只有主内容区变化。


Part 5: 动态路由参数

URL 参数(:id)

很多页面需要根据 URL 参数显示不同内容,比如:

  • /users/1 显示用户 1 的信息
  • /users/2 显示用户 2 的信息
  • /posts/123 显示文章 123 的内容

React Router 用 :paramName 定义动态参数:

ini 复制代码
<Route path="/users/:id" element={<UserDetail />} />

这里的 :id 就是动态参数,它可以匹配任何值。

useParams Hook

在组件里用 useParams 获取 URL 参数:

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

function UserDetail() {
  const { id } = useParams();

  return <h1>用户 {id} 的详情</h1>;
}

当 URL 是 /users/1 时,id 就是 "1";当 URL 是 /users/2 时,id 就是 "2"

实战:用户详情页

javascript 复制代码
import { useParams } from 'react-router-dom';
import { useState, useEffect } from 'react';

function UserDetail() {
  const { id } = useParams();
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${id}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, [id]); // id 变化时重新获取

  if (loading) return <p>加载中...</p>;
  if (!user) return <p>用户不存在</p>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>邮箱:{user.email}</p>
    </div>
  );
}

注意 useEffect 的依赖数组是 [id]------当 id 变化时,重新获取用户数据。

查询参数(?key=value)

除了路径参数,URL 还可以有查询参数,比如 /search?q=react&page=2

React Router 用 useSearchParams 处理查询参数:

javascript 复制代码
import { useSearchParams } from 'react-router-dom';

function SearchPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const query = searchParams.get('q');
  const page = searchParams.get('page') || '1';

  return (
    <div>
      <p>搜索:{query}</p>
      <p>第 {page} 页</p>
      <button onClick={() => setSearchParams({ q: query, page: '2' })}>
        下一页
      </button>
    </div>
  );
}

useSearchParams 返回一个数组:

  • searchParams:当前的查询参数对象
  • setSearchParams:更新查询参数的函数

路径参数 vs 查询参数

路径参数 查询参数
URL 格式 /users/123 /users?id=123
用途 资源标识(必须的) 可选的过滤、排序、分页
获取方式 useParams() useSearchParams()
典型场景 用户详情、文章详情 搜索、筛选、分页

个人建议:如果参数是资源的唯一标识(比如用户 ID),用路径参数;如果是可选的过滤条件(比如搜索关键词),用查询参数。


Part 6: 编程式导航

useNavigate Hook

有时候你需要在代码里控制跳转,比如:

  • 用户登录成功后,跳转到首页
  • 表单提交成功后,跳转到列表页
  • 用户没有权限时,跳转到登录页

React Router 用 useNavigate Hook 实现编程式导航:

javascript 复制代码
import { useNavigate } from 'react-router-dom';

function LoginPage() {
  const navigate = useNavigate();

  async function handleLogin(e) {
    e.preventDefault();
    const success = await login(username, password);
    if (success) {
      navigate('/'); // 跳转到首页
    }
  }

  return (
    <form onSubmit={handleLogin}>
      {/* ... */}
    </form>
  );
}

导航到指定页面

scss 复制代码
const navigate = useNavigate();

// 跳转到 /about
navigate('/about');

// 跳转到 /users/123
navigate('/users/123');

// 跳转到 /search?q=react&page=2
navigate('/search?q=react&page=2');

后退和前进

scss 复制代码
const navigate = useNavigate();

// 后退
navigate(-1);

// 前进
navigate(1);

// 后退两步
navigate(-2);

导航时传递状态

有时候你想在跳转时传递一些数据,但不想放在 URL 里。可以用 state

php 复制代码
const navigate = useNavigate();

// 跳转时传递状态
navigate('/dashboard', { state: { from: 'login' } });

在目标页面读取状态:

javascript 复制代码
import { useLocation } from 'react-router-dom';

function Dashboard() {
  const location = useLocation();
  const from = location.state?.from;

  return <p>你从 {from} 页面来</p>;
}

这个状态不会出现在 URL 里,但可以在页面间传递。适合传递一些临时数据,比如"从哪个页面跳过来的"。


Part 7: 404 页面和重定向

通配符路由

当用户访问一个不存在的 URL 时,你需要显示 404 页面。用通配符 * 匹配所有路径:

xml 复制代码
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  <Route path="*" element={<NotFound />} /> {/* 404 */}
</Routes>

React Router 会按顺序匹配路由,如果前面的都不匹配,就会匹配 *

有时候你需要把用户重定向到另一个页面,比如:

  • 用户访问 /old-page,自动跳转到 /new-page
  • 用户没有登录,自动跳转到 /login

React Router 用 Navigate 组件实现重定向:

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

function OldPage() {
  return <Navigate to="/new-page" replace />;
}

replace 表示替换当前历史记录,而不是添加一条新记录。这样用户后退时不会回到旧页面。

实战:登录重定向

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

function ProtectedRoute({ children }) {
  const isLoggedIn = checkAuth(); // 检查是否登录

  if (!isLoggedIn) {
    return <Navigate to="/login" replace />;
  }

  return children;
}

// 使用
<Routes>
  <Route path="/login" element={<Login />} />
  <Route
    path="/dashboard"
    element={
      <ProtectedRoute>
        <Dashboard />
      </ProtectedRoute>
    }
  />
</Routes>

这样,如果用户没有登录就访问 /dashboard,会被自动重定向到 /login


Part 8: 实战:完整的路由示例

把前面所有知识点整合起来,做一个小型博客系统的路由设计:

javascript 复制代码
import { BrowserRouter, Routes, Route, Link, Outlet, useParams, useNavigate } from 'react-router-dom';

// 布局组件
function Layout() {
  return (
    <div>
      <header>
        <nav>
          <Link to="/">首页</Link>
          <Link to="/posts">文章</Link>
          <Link to="/about">关于</Link>
        </nav>
      </header>
      <main>
        <Outlet />
      </main>
    </div>
  );
}

// 首页
function Home() {
  return <h1>欢迎来到我的博客</h1>;
}

// 文章列表
function PostList() {
  const posts = [
    { id: 1, title: '第一篇文章' },
    { id: 2, title: '第二篇文章' },
  ];

  return (
    <div>
      <h2>文章列表</h2>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <Link to={`/posts/${post.id}`}>{post.title}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

// 文章详情
function PostDetail() {
  const { id } = useParams();
  const navigate = useNavigate();

  return (
    <div>
      <h2>文章 {id} 的详情</h2>
      <button onClick={() => navigate('/posts')}>返回列表</button>
    </div>
  );
}

// 关于页面
function About() {
  return <h1>关于我</h1>;
}

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

// App
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="posts" element={<PostList />} />
          <Route path="posts/:id" element={<PostDetail />} />
          <Route path="about" element={<About />} />
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

这个例子展示了:

  • 布局组件(Layout)+ 嵌套路由
  • 动态参数(/posts/:id
  • 编程式导航(navigate('/posts')
  • 404 页面(* 通配符)

总结

  • 路由的本质:让 URL 和 UI 保持同步,解决单页应用的 URL "失联"问题
  • BrowserRouter:使用 History API,URL 更干净,需要服务器配合
  • Route:声明"什么 URL 显示什么组件"
  • Link :声明式导航,替代 <a> 标签
  • 嵌套路由 :用 Outlet 实现布局复用
  • 动态参数 :用 :paramName 定义路径参数,用 useParams 获取
  • 编程式导航 :用 useNavigate 在代码里控制跳转
  • 404 页面 :用 * 通配符匹配所有未定义的路径

路由是单页应用的基础架构。没有路由,你的应用就只是一个"大组件";有了路由,它才真正变成一个"网站"。


相关资源


本文基于 React Router v6+ 版本。

相关推荐
Lkstar5 小时前
Vue Router 进阶:导航守卫、动态路由与懒加载,源码级理解
前端
ricardo19735 小时前
# Tree Shaking 深度解析:为什么你的代码没被摇掉?
前端·面试
前端流一5 小时前
踩坑实录:Vite打包AntD5报错 rc-picker/es/generate/dayjs 模块找不到
前端
_按键伤人_5 小时前
三、手把手教你从零写一个本地 RAG
前端·llm·ai编程
008爬虫实战录5 小时前
【码上爬】 题十二:如来神掌 困难, JSVMP加密,使用代理补环境
前端·javascript·node.js
008爬虫实战录5 小时前
【码上爬】 题十:魔改算法 堆栈分析,找加密值过程详解
前端·python·算法
无人装备硬件开发爱好者5 小时前
深度解析GPS天线设计:从贴片天线到LNA前端的完整硬件方案
前端
卷帘依旧6 小时前
React Hook采用环形链表的原因
前端
lichenyang4536 小时前
从 HarmonyOS AI 聊天模块理解工程化架构:MVVM、Controller、Provider、请求封装与 SSE
前端