React Router v6 入门教程:构建你的第一个单页应用导航
欢迎来到 React 的世界!在你深入构建复杂的交互式界面时,很快会遇到一个核心问题:如何在不同的“页面”或视图之间切换,而不需要传统的浏览器页面刷新?这就是前端路由的用武之地,而 React Router 是 React 生态系统中最流行、功能最强大的路由库。
本教程将详细介绍 React Router 的基本概念、核心组件和常见用法,特别是针对最新的 v6 版本。我们将一步步构建一个简单的应用,学习如何定义路由、实现导航、处理动态参数以及构建基本的布局。
目标读者: 已经具备基本的 React 知识,并希望学习如何在 React 应用中实现前端路由的开发者。
我们将涵盖以下内容:
- 什么是前端路由?为什么单页应用(SPA)需要它?
- 为什么选择 React Router?
- 安装 React Router v6
- 核心概念与组件:
BrowserRouter
(或其他 Router)Routes
Route
Link
- 构建一个简单的路由应用
- 处理路由参数 (
useParams
) - 嵌套路由 (
Outlet
和相对路径) - 编程式导航 (
useNavigate
) - 处理 404 (Not Found) 页面
- 常见问题与最佳实践
1. 什么是前端路由?为什么单页应用(SPA)需要它?
在传统的 Web 开发中,每次用户点击链接或提交表单,浏览器都会向服务器发送请求,服务器处理请求后返回一个新的 HTML 页面。这个过程导致整个页面被重新加载,用户体验会受到中断。
单页应用(Single Page Application – SPA)的目标是提供更流畅、类似原生应用的体验。SPA 在初次加载时只下载一次 HTML、CSS 和 JavaScript 资源,后续的用户交互(如点击导航链接)不会触发整个页面的刷新。相反,JavaScript 会动态地改变页面上的内容。
但问题来了:虽然页面没有刷新,但用户仍然期望浏览器的前进/后退按钮能够工作,地址栏的 URL 能够反映当前“视图”的状态,并且能够通过 URL 直接访问应用的某个特定视图(书签、分享链接)。
这就是前端路由的作用。前端路由库(如 React Router)通过监听浏览器地址栏的变化,并根据 URL 匹配相应的组件或视图,然后将这些组件渲染到页面上,同时更新浏览器历史记录。这个过程完全由客户端的 JavaScript 控制,无需与服务器进行页面的完整交互。
简单来说:
- 传统多页应用 (MPA): URL 变化 -> 浏览器向服务器请求新页面 -> 服务器返回新 HTML -> 浏览器加载并显示新页面 (页面刷新)。
- 单页应用 (SPA) + 前端路由: URL 变化 -> 前端路由库监听并匹配 URL -> JavaScript 动态渲染对应组件 -> 更新浏览器历史记录 (无页面刷新)。
2. 为什么选择 React Router?
React Router 是 React 社区中最成熟、最受欢迎的路由解决方案。它具有以下优点:
- 声明式 API: 使用 React 组件的方式定义路由,与 React 的开发模式高度契合。
- 灵活性强: 可以很容易地实现各种路由需求,包括基本路由、嵌套路由、参数传递、重定向等。
- 活跃的社区和丰富的文档: 遇到问题容易找到解决方案,资源丰富。
- 支持多种环境: 不仅用于 Web (react-router-dom),也支持 React Native (react-router-native)。
- Hooks 支持: 在函数式组件中方便地使用路由功能(
useHistory
,useLocation
,useParams
,useRouteMatch
在 v5 中;useNavigate
,useLocation
,useParams
,useMatch
在 v6 中)。
本教程将专注于 react-router-dom
,它是用于 Web 应用的版本。
3. 安装 React Router v6
首先,确保你已经创建了一个 React 项目(可以使用 Create React App、Vite 或其他方式)。
打开你的项目终端,运行以下命令安装 react-router-dom
:
使用 npm:
bash
npm install react-router-dom
使用 yarn:
bash
yarn add react-router-dom
安装完成后,就可以开始使用 React Router 了。
4. 核心概念与组件
React Router v6 相对于 v5 有一些重要的改变,最明显的是引入了 Routes
组件和新的路由匹配方式。理解以下几个核心组件是掌握 React Router 的关键。
BrowserRouter
(或其他 Router 组件)
BrowserRouter
是 React Router 在 Web 应用中最常用的 Router 组件。它使用 HTML5 History API (pushState, replaceState, popstate 事件) 来保持 UI 与 URL 的同步。这意味着你的 URL 看起来是标准的路径,例如 /about
或 /users/123
。
你需要在应用的最顶层包裹 BrowserRouter
,通常是在 src/index.js
或 src/App.js
中。
“`jsx
// src/index.js
import React from ‘react’;
import ReactDOM from ‘react-dom/client’; // For React 18+
import { BrowserRouter } from ‘react-router-dom’;
import App from ‘./App’;
const root = ReactDOM.createRoot(document.getElementById(‘root’));
root.render(
{/ 将你的整个应用包裹在 BrowserRouter 中 /}
);
“`
注意: 还有其他 Router 组件,例如 HashRouter
(使用 URL 的哈希部分 #
来管理路由,例如 /about
会变成 /#/about
,兼容性更好但 URL 不美观,不推荐用于新项目) 和 MemoryRouter
(在内存中管理路由,常用于测试或非浏览器环境)。对于大多数 Web 应用,BrowserRouter
是首选。
Routes
Routes
是 React Router v6 中一个重要的概念。它取代了 v5 中的 Switch
。
Routes
的作用是遍历其所有的Route
子元素,并渲染第一个与当前 URL 最佳匹配的Route
。- 在 v6 中,你必须将所有的
Route
组件放在Routes
组件内部。
“`jsx
// 在你的路由定义的地方(通常在 App.js 或单独的路由文件中)
import { Routes, Route } from ‘react-router-dom’;
// 导入你的页面组件
import Home from ‘./pages/Home’;
import About from ‘./pages/About’;
import Contact from ‘./pages/Contact’;
function App() {
return (
{/* Routes 组件包裹所有的 Route */}
<Routes>
{/* Route 组件定义具体的路由规则 */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
{/* 页面底部或其他全局内容 */}
<footer>...</footer>
</div>
);
}
“`
Route
Route
是定义特定路由规则的核心组件。它有两个主要的 props:
path
: 一个字符串,表示这个路由匹配的 URL 路径。它可以是精确的路径(如/about
)、动态路径(如/users/:userId
)或通配符路径(如*
)。element
: 一个 React 元素(即 JSX),当path
与当前 URL 匹配时,这个元素会被渲染。
jsx
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
在 v6 中,你不再使用 component
或 render
prop 来指定要渲染的组件,而是使用 element
prop 并传入一个 React 元素(例如 <Home />
)。
Link
Link
组件用于在应用内部创建导航链接。它取代了传统的 <a>
标签。使用 Link
可以防止浏览器进行完整的页面刷新,而是由 React Router 拦截点击事件,通过 History API 来改变 URL 并触发路由匹配,从而实现 SPA 的无刷新导航。
Link
组件的主要 prop 是 to
,它指定了要导航到的路径。
“`jsx
import { Link } from ‘react-router-dom’;
function Navigation() {
return (
);
}
“`
你可以将 Link
组件放在任何地方,例如你的全局导航栏、列表项或按钮中。
重要提示: 只有当你需要在应用内部进行页面跳转时才使用 <Link>
。如果你需要跳转到外部网站,或者强制进行页面刷新(尽管在 SPA 中这很少见),仍然应该使用标准的 <a>
标签。
5. 构建一个简单的路由应用
让我们把上面介绍的核心组件组合起来,构建一个包含首页、关于页和联系页的简单应用。
项目结构示例:
my-react-app/
├── public/
│ └── index.html
├── src/
│ ├── index.js
│ ├── App.js
│ └── pages/
│ ├── Home.js
│ ├── About.js
│ └── Contact.js
└── package.json
1. 安装 React Router (已完成)
2. 创建页面组件 (src/pages/)
src/pages/Home.js
:
“`jsx
import React from ‘react’;
function Home() {
return (
首页
欢迎来到我们的网站!
);
}
export default Home;
“`
src/pages/About.js
:
“`jsx
import React from ‘react’;
function About() {
return (
关于我们
我们是一家很棒的公司。
);
}
export default About;
“`
src/pages/Contact.js
:
“`jsx
import React from ‘react’;
function Contact() {
return (
联系我们
你可以通过以下方式联系我们…
);
}
export default Contact;
“`
3. 在 src/index.js
中设置 BrowserRouter
确保你的 index.js
看起来像这样(如果使用 React 18+):
“`jsx
import React from ‘react’;
import ReactDOM from ‘react-dom/client’;
import { BrowserRouter } from ‘react-router-dom’;
import App from ‘./App’;
import ‘./index.css’; // 导入你的 CSS 文件
const root = ReactDOM.createRoot(document.getElementById(‘root’));
root.render(
);
或者对于旧版本 React:
jsx
// src/index.js (For React < 18)
import React from ‘react’;
import ReactDOM from ‘react-dom’;
import { BrowserRouter } from ‘react-router-dom’;
import App from ‘./App’;
import ‘./index.css’; // 导入你的 CSS 文件
ReactDOM.render(
document.getElementById(‘root’)
);
“`
4. 在 src/App.js
中定义路由和导航
“`jsx
import React from ‘react’;
import { Routes, Route, Link } from ‘react-router-dom’;
import Home from ‘./pages/Home’;
import About from ‘./pages/About’;
import Contact from ‘./pages/Contact’;
import ‘./App.css’; // 导入你的 CSS 文件
function App() {
return (
我的路由应用
<main>
{/* 在这里定义路由 */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</main>
<footer>
<p>© 2023 我的应用</p>
</footer>
</div>
);
}
export default App;
“`
现在运行你的 React 应用(通常是 npm start
或 yarn start
),你就可以通过点击导航链接在不同的页面之间无刷新切换,并且地址栏的 URL 会相应改变。尝试手动在地址栏输入 /about
或 /contact
,你会发现页面也会正确地加载对应的组件。
6. 处理路由参数 (useParams
)
很多时候,你需要根据 URL 中的一部分动态地加载内容。例如,一个用户列表页面可能需要点击某个用户跳转到 /users/123
这样的 URL,其中 123
是用户的 ID。React Router 允许你在路由路径中定义参数,并在组件中通过 useParams
Hook 获取这些参数。
1. 定义带参数的路由:
在 Route
的 path
中使用冒号 :
来定义参数名。
jsx
<Route path="/users/:userId" element={<UserProfile />} />
这里的 :userId
就是一个动态参数。
2. 创建一个需要参数的组件:
使用 useParams
Hook 获取 URL 中的参数对象。
创建一个 src/pages/UserProfile.js
:
“`jsx
import React from ‘react’;
import { useParams } from ‘react-router-dom’;
function UserProfile() {
// useParams 返回一个对象,其键是你在 Route path 中定义的参数名
const { userId } = useParams();
// 你可以使用 userId 来请求后端数据或进行其他操作
const user = {
123: { name: ‘张三’, age: 30 },
456: { name: ‘李四’, age: 25 },
}[userId] || null; // 模拟获取用户数据
if (!user) {
return
;
}
return (
用户详情
用户 ID: {userId}
姓名: {user.name}
年龄: {user.age}
);
}
export default UserProfile;
“`
3. 更新 App.js
添加带参数的路由和链接:
“`jsx
import React from ‘react’;
import { Routes, Route, Link } from ‘react-router-dom’;
// … 导入其他页面组件
import UserProfile from ‘./pages/UserProfile’; // 导入 UserProfile 组件
function App() {
return (
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
{/* 添加带参数的路由 */}
<Route path="/users/:userId" element={<UserProfile />} />
</Routes>
</main>
{/* ... 底部 */}
</div>
);
}
“`
现在,当你点击“用户张三”或“用户李四”的链接时,URL 会变成 /users/123
或 /users/456
,并且 UserProfile
组件会渲染,通过 useParams
获取到相应的 userId
并显示对应的信息。
7. 嵌套路由 (Outlet
和相对路径)
在复杂的应用中,某个页面的内容可能又包含多个子视图,并且这些子视图也有对应的 URL。例如,一个 Dashboard 页面可能有概览 (/dashboard
)、设置 (/dashboard/settings
) 和用户管理 (/dashboard/users
) 等子页面,它们都共享一个共同的布局(侧边栏、顶部导航)。这就是嵌套路由的应用场景。
React Router v6 处理嵌套路由的方式是:
- 在父级
Route
的path
中使用通配符/*
(如果你希望该路径下的所有子路径都能被匹配到)。 - 父级
Route
的element
对应的组件负责渲染共享的布局和子路由应该渲染的位置。 - 在父级
Routes
中,定义子Route
,它们的path
是相对于父级路径的。 - 父级布局组件中使用
Outlet
组件来标记子路由渲染的位置。
1. 定义父级和子级路由:
修改 App.js
中的 Routes
:
“`jsx
import React from ‘react’;
import { Routes, Route, Link } from ‘react-router-dom’;
// … 导入其他页面组件
import UserProfile from ‘./pages/UserProfile’;
import DashboardLayout from ‘./pages/DashboardLayout’; // 导入 DashboardLayout
import DashboardHome from ‘./pages/DashboardHome’; // 导入 DashboardHome
import DashboardSettings from ‘./pages/DashboardSettings’; // 导入 DashboardSettings
function App() {
return (
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="/users/:userId" element={<UserProfile />} />
{/* 定义父级路由,使用 /* 表示匹配 /dashboard/ 开头的任何路径 */}
<Route path="/dashboard/*" element={<DashboardLayout />}>
{/*
在这里定义子路由。
注意:这些 path 是相对于父级 path (/dashboard/) 的。
path="" 表示匹配父级路径本身 (/dashboard)。
path="settings" 表示匹配 /dashboard/settings。
不需要再次写 /dashboard/。
*/}
<Route path="" element={<DashboardHome />} /> {/* 匹配 /dashboard */}
<Route path="settings" element={<DashboardSettings />} /> {/* 匹配 /dashboard/settings */}
{/* 可以添加一个嵌套的 404 路由,它只会在 /dashboard/* 下匹配不到时生效 */}
<Route path="*" element={<div>Dashboard 子页面未找到</div>} />
</Route>
</Routes>
</main>
{/* ... 底部 */}
</div>
);
}
“`
2. 创建父级布局组件 (src/pages/DashboardLayout.js):
这个组件将包含共享的布局和 Outlet
。
“`jsx
import React from ‘react’;
import { Link, Outlet } from ‘react-router-dom’;
function DashboardLayout() {
return (
Dashboard 布局
这是 Dashboard 的共享布局(例如侧边栏和头部)。
<nav>
<ul>
<li>
{/* Link to="" 相当于 Link to="/dashboard" 因为父级 path 是 /dashboard/* */}
<Link to="/dashboard">Dashboard 概览</Link>
</li>
<li>
{/* Link to="settings" 相当于 Link to="/dashboard/settings" */}
<Link to="/dashboard/settings">Dashboard 设置</Link>
</li>
{/* 也可以使用绝对路径 */}
<li>
<Link to="/about">回到关于页面 (绝对路径)</Link>
</li>
</ul>
</nav>
<hr /> {/* 分隔线 */}
{/* Outlet 是子路由元素应该渲染的位置 */}
<div style={{ border: '1px dashed blue', padding: '10px' }}>
<h3>子路由内容渲染区域:</h3>
<Outlet />
</div>
</div>
);
}
export default DashboardLayout;
“`
3. 创建子页面组件 (src/pages/DashboardHome.js, src/pages/DashboardSettings.js):
这些是实际渲染在 Outlet
位置的组件。
src/pages/DashboardHome.js
:
“`jsx
import React from ‘react’;
function DashboardHome() {
return (
Dashboard 概览
欢迎来到 Dashboard 概览页面。
);
}
export default DashboardHome;
“`
src/pages/DashboardSettings.js
:
“`jsx
import React from ‘react’;
function DashboardSettings() {
return (
Dashboard 设置
在这里配置你的 Dashboard 设置。
);
}
export default DashboardSettings;
“`
现在,当你访问 /dashboard
时,会渲染 DashboardLayout
,并且 Outlet
位置会渲染 DashboardHome
。当你访问 /dashboard/settings
时,仍然渲染 DashboardLayout
,但 Outlet
位置会渲染 DashboardSettings
。注意导航链接 to
prop 中的相对路径用法,这在嵌套路由中非常方便。
8. 编程式导航 (useNavigate
)
有时你需要在代码中触发导航,而不是通过点击 Link
。例如,用户成功登录后自动跳转到 Dashboard 页面,或者表单提交成功后跳转到结果页。React Router v6 提供了 useNavigate
Hook 来实现编程式导航。
useNavigate
Hook 返回一个函数,调用这个函数就可以进行导航。
“`jsx
import React from ‘react’;
import { useNavigate } from ‘react-router-dom’;
function LoginForm() {
const navigate = useNavigate(); // 获取导航函数
const handleSubmit = (event) => {
event.preventDefault();
// 假设登录成功
const loginSuccessful = true; // 实际中会是验证逻辑
if (loginSuccessful) {
// 使用 navigate 函数进行跳转
// navigate('/dashboard'); // 跳转到 /dashboard
// navigate('/users/me'); // 跳转到 /users/me
navigate('/dashboard', { replace: true }); // 跳转并替换当前历史记录项
// 也可以跳转回上一页或前进
// navigate(-1); // 返回上一页
// navigate(1); // 前进一页
} else {
// 处理登录失败
alert('登录失败!');
}
};
return (
);
}
export default LoginForm;
“`
你可以将这个 LoginForm
组件添加到你的应用中,例如在 App.js
中添加一个 /login
路由。当表单提交时,如果登录成功,useNavigate
返回的函数就会被调用,实现页面跳转。
navigate
函数可以接受多种参数:
- 字符串路径:
navigate('/some/path')
- 数字:
navigate(-1)
(后退),navigate(1)
(前进) - 对象:
navigate('/some/path', { replace: true, state: { from: location.pathname } })
replace: true
会替换历史堆栈中的当前条目,而不是添加一个新条目。state
: 可以传递一些状态数据,在目标组件中通过useLocation().state
获取。
9. 处理 404 (Not Found) 页面
当用户访问一个不存在的 URL 时,你通常希望显示一个“页面未找到”的友好提示。在 React Router v6 中,你可以通过在 <Routes>
的最后定义一个 path="*"
的 Route
来实现这一点。
通配符 *
会匹配任何路径。由于 Routes
会渲染第一个最佳匹配的 Route
,将 path="*"
的 Route
放在 <Routes>
的最后非常重要。这样,只有当所有其他更具体的路由(/
, /about
, /users/:userId
, /dashboard/*
等)都无法匹配当前 URL 时,path="*"
的路由才会生效。
1. 创建一个 404 页面组件:
创建一个 src/pages/NotFound.js
:
“`jsx
import React from ‘react’;
import { Link } from ‘react-router-dom’;
function NotFound() {
return (
404 页面未找到
抱歉,你访问的页面不存在。
你可以 回到首页。
);
}
export default NotFound;
“`
2. 在 App.js
中添加 404 路由:
确保它在 Routes
的最后。
“`jsx
import React from ‘react’;
import { Routes, Route, Link } from ‘react-router-dom’;
// … 导入其他页面组件
import NotFound from ‘./pages/NotFound’; // 导入 NotFound 组件
function App() {
return (
<main>
<Routes>
{/* 定义其他所有具体的路由 */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="/users/:userId" element={<UserProfile />} />
<Route path="/dashboard/*" element={<DashboardLayout />}>
<Route path="" element={<DashboardHome />} />
<Route path="settings" element={<DashboardSettings />} />
{/* 注意:这里的 * 只匹配 /dashboard/* 下未匹配的路径 */}
{/* <Route path="*" element={<div>Dashboard 子页面未找到</div>} /> */}
</Route>
{/*
定义 404 路由。
重要:将其放在所有其他路由的最后!
*/}
<Route path="*" element={<NotFound />} />
</Routes>
</main>
{/* ... 底部 */}
</div>
);
}
“`
现在,当你访问如 /abcxyz
这样不存在的路径时,会显示你创建的 404 页面。
10. 常见问题与最佳实践
BrowserRouter
的 fallback: 使用BrowserRouter
时,如果用户直接访问一个非根路径的 URL (例如http://localhost:3000/about
),服务器需要在处理这个请求时返回应用的 HTML 文件(通常是index.html
),而不是尝试去查找一个实际存在的/about
资源。然后 React Router 会接管并在客户端根据路径渲染正确的组件。在开发环境中,如 Create React App 或 Vite,这通常已经配置好了。但在生产环境中部署时,你需要配置你的 Web 服务器(如 Nginx, Apache, Netlify, Vercel 等)来将所有指向你的应用路径的请求都重写或重定向到index.html
。Routes
必须包裹Route
: 在 v6 中,Route
组件必须是Routes
的直接子元素(或者嵌套在另一个Route
的element
prop 中定义子路由,如我们在嵌套路由示例中所示)。你不能在Routes
之外随意放置Route
。- 使用
Link
进行内部导航: 始终使用<Link>
组件进行应用内部的页面跳转,以获得无刷新体验。 index
Routes (v6): 在嵌套路由中,<Route path="" element={<Component />} />
被称为索引路由。它会匹配父级路由的精确路径(例如/dashboard
)。这是 v6 中推荐的处理父级路径对应组件的方式,取代了 v5 中<Route exact path="/dashboard">
的部分功能。- 相对路径 vs. 绝对路径: 在
<Link to="...">
和<Route path="...">
中都可以使用相对路径。相对路径(如to="settings"
或path="settings"
) 是相对于当前匹配的路由路径的。绝对路径(如to="/dashboard/settings"
或path="/dashboard/settings"
) 始终从应用的根路径 (/
) 开始。在嵌套路由中,使用相对路径通常更方便和灵活。 - 其他有用的 Hooks: React Router 还提供其他一些有用的 Hooks:
useLocation()
: 返回当前 URL 的位置对象,包含pathname
(路径),search
(查询字符串),hash
(哈希值) 等信息。useMatch(pattern)
: 检查当前 URL 是否与给定的模式匹配,返回匹配对象或null
。这在判断某个 Link 是否应该被激活时很有用(虽然NavLink
通常是更好的选择)。
NavLink
: 如果你想在当前路由激活时给导航链接添加一个特殊的样式(例如高亮当前页面链接),可以使用NavLink
组件代替Link
。它提供了activeClassName
(v5) 或className
(v6, 函数式用法) 和activeStyle
(v5) 或style
(v6, 函数式用法) props 来实现这个功能。在 v6 中,通常是<NavLink to="/about" className={({ isActive }) => isActive ? 'active' : ''}>关于</NavLink>
。
总结
恭喜你!你已经学习了 React Router v6 的核心概念和常用功能。你现在应该能够:
- 理解为什么需要前端路由以及 React Router 的作用。
- 安装和设置
react-router-dom
。 - 使用
BrowserRouter
包裹你的应用。 - 使用
Routes
和Route
定义页面路由。 - 使用
Link
实现应用内部的导航。 - 通过
useParams
获取 URL 参数。 - 通过嵌套
Route
和Outlet
构建包含共享布局的子路由。 - 使用
useNavigate
实现编程式导航。 - 设置一个 404 页面来处理未匹配的路径。
这仅仅是 React Router 功能的冰山一角。它还支持更多高级特性,如:
- 路由懒加载 (使用
React.lazy
和Suspense
) useSearchParams
Hook (处理 URL 查询字符串)- 更复杂的重定向逻辑
- 数据加载 (v6.4+ 的 loaders) 和数据提交 (v6.4+ 的 actions)
但掌握了本教程中的基础知识,你就已经为构建一个具备完整导航功能的 React 单页应用奠定了坚实的基础。
下一步:
- 尝试在你的实际项目中应用这些知识。
- 阅读 React Router 官方文档,探索更高级的功能和 API 详情。
- 学习如何配合
React.lazy
和Suspense
实现路由的按需加载,优化应用性能。
祝你在 React Router 的学习旅程中顺利!