创作者: Yardon  |  GitHub: github.com/YardonYan  |  版本: v1.0


单页应用的 URL 问题

传统的多页网站,每个页面有自己的 URL。你在浏览器地址栏输入 /about,服务器就返回 about.html,每一页都是完整独立的一次往返。

React 做的单页应用(SPA)不一样——浏览器加载一个 HTML 文件之后,所有的页面切换都在前端完成,服务器只负责提供 JSON 数据。

这就带来一个问题:URL 怎么变?

用户期望的行为是——点击导航、刷新页面、前进后退都能正常工作。这意味着 React 必须"接管"浏览器的 URL,在不刷新页面的前提下改变地址栏的内容。

React Router 就是做这件事的。它的核心思想很简单:

把 URL 当成一个状态,当它变化时,渲染对应的组件。


React Router v6 快速上手

安装与基础配置

npm install react-router-dom
// main.jsx
import { BrowserRouter } from 'react-router-dom';
import App from './App';

createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);
// App.jsx
import { Routes, Route } from 'react-router-dom';

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/blog" element={<Blog />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

Link 和 NavLink

import { Link, NavLink } from 'react-router-dom';

function Navbar() {
  return (
    <nav>
      {/* 基础链接 */}
      <Link to="/">首页</Link>

      {/* NavLink 会自动给当前页面高亮 */}
      <NavLink
        to="/blog"
        className={({ isActive }) => isActive ? 'active' : ''}
      >
        博客
      </NavLink>
    </nav>
  );
}

NavLinkLink 的增强版——它知道自己是不是当前页,可以自动应用激活样式。

编程式导航

有时候需要在代码里控制跳转(比如表单提交成功后跳转到新页面):

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

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

  async function handleSubmit(e) {
    e.preventDefault();
    const result = await login(username, password);
    if (result.success) {
      navigate('/dashboard');  // 成功后跳转
    }
  }
}

路由嵌套与 Outlet

一个常见的布局模式:顶部导航栏、侧边栏是固定的,中间的内容区域跟随路由变化。

┌─────────────────────────────┐
│      Navbar (固定)           │
├────────┬────────────────────┤
│Sidebar │  内容区域 (变化)     │
│ (固定) │                    │
│        │                    │
└────────┴────────────────────┘
function App() {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        {/* 这些路由的内容会渲染到 Layout 的 Outlet 位置 */}
        <Route index element={<Home />} />
        <Route path="dashboard" element={<Dashboard />} />
        <Route path="settings" element={<Settings />} />
      </Route>
    </Routes>
  );
}

function Layout() {
  return (
    <div>
      <Navbar />
      <div className="main">
        <Sidebar />
        <div className="content">
          <Outlet />  {/* 子路由的内容渲染在这里 */}
        </div>
      </div>
    </div>
  );
}

<Outlet /> 就是路由的「插槽」——子路由匹配到哪个组件,它就渲染哪个组件。而 Layout 组件本身保持不变(导航栏、侧边栏不用重新渲染)。这个特性在搭建复杂布局时非常关键。


动态路由与 URL 参数

博客文章页面,每篇文章有一个唯一 ID。URL 大概是 /blog/123,其中 123 是动态的。

<Route path="/blog/:id" element={<BlogPost />} />
import { useParams } from 'react-router-dom';

function BlogPost() {
  const { id } = useParams();  // id = "123"

  const [post, setPost] = useState(null);

  useEffect(() => {
    fetch(`/api/posts/${id}`)
      .then(res => res.json())
      .then(setPost);
  }, [id]);

  if (!post) return <Loading />;
  return <PostDetail post={post} />;
}

导航守卫与重定向

保护需要登录的页面

function ProtectedRoute({ children }) {
  const isLoggedIn = useAuthStore(s => s.isLoggedIn);

  if (!isLoggedIn) {
    // 未登录 → 跳到登录页,并记住想去的页面
    return <Navigate to="/login" replace />;
  }

  return children;
}

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

旧路径重定向到新路径

// 旧版路径自动跳转
<Route path="/old-about" element={<Navigate to="/about" replace />} />

查询参数与 useSearchParams

博客列表页的翻页和筛选一般用查询参数实现:/blog?page=2&tag=react

import { useSearchParams } from 'react-router-dom';

function BlogList() {
  const [searchParams, setSearchParams] = useSearchParams();

  const page = Number(searchParams.get('page')) || 1;
  const tag = searchParams.get('tag') || '';

  function goToPage(n) {
    setSearchParams({ page: n, tag });  // 保留其他参数
  }

  return (
    <div>
      {/* 翻页按钮 */}
      <button onClick={() => goToPage(page - 1)} disabled={page === 1}>上一页</button>
      <span>第 {page} 页</span>
      <button onClick={() => goToPage(page + 1)}>下一页</button>
    </div>
  );
}

懒加载与代码分割

当应用变大后,所有页面打包进一个 JS 文件会导致首屏加载很慢。React Router 配合 lazy 实现按需加载:

import { lazy, Suspense } from 'react';

// 每个页面单独打包,访问时才加载
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

Suspense 在组件加载过程中显示一个 fallback(加载动画),让用户知道"页面正在加载",而不是看一片空白。


本章小结

概念 一句话总结
BrowserRouter 包装整个应用,启用前端路由
Routes/Route 声明式定义 URL 到组件的映射
Link/NavLink 无刷新的页面跳转,NavLink 自带激活判断
Outlet 嵌套路由的插槽,布局组件渲染子路由内容
useParams 读取动态路由参数 /blog/:id
Navigate 声明式重定向
useSearchParams 读写查询参数 /blog?page=2
lazy + Suspense 按需加载页面,减小首屏包体积

路由是现代前端框架的基石。掌握这些概念后,你就拥有了搭建完整多页 SPA 的能力。下一章,我们聚焦 性能优化——如何让 React 应用更快、更流畅。


📌 创作者: Yardon  |  🏠 个人网站: GlimmerAI.top

📖 本章是「React 从入门到生产」系列的第 6 章。上一章:状态管理选型 | 下一章:性能优化

🌟 如果你觉得有帮助,欢迎访问 GlimmerAI.top 查看我的更多作品。欢迎大家来观看!

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐