征服单页应用导航:React Router v6 深度入门教程
单页应用 (SPA) 带来了流畅的用户体验,页面切换无需重新加载,感觉就像桌面应用一样。然而,这也引入了一个新的挑战:如何在不进行传统页面刷新的情况下,实现页面之间的跳转、处理不同的 URL,并让用户能够使用浏览器的前进/后退按钮?
这就是客户端路由的职责,而 React Router 是 React 社区中最流行、功能最强大的客户端路由库之一。它让你能够将不同的 URL 地址映射到不同的 React 组件,从而在视觉上模拟多页应用的行为,但保留 SPA 的性能优势。
本教程将带你从零开始,深入理解 React Router v6 的核心概念和使用方法,并通过实际代码示例构建一个简单的多页面应用。
为什么需要客户端路由?
在传统的 Web 开发中,每次用户点击一个链接或提交一个表单,浏览器都会向服务器发送一个请求,服务器返回一个新的 HTML 页面,浏览器清除旧页面并渲染新页面。这被称为服务器端路由。
在 SPA 中,初始加载时通常只会下载一个 HTML 文件(通常是 index.html
),然后所有的页面内容都是通过 JavaScript 动态生成和更新的。在这种模式下:
- 用户体验: 切换页面时无需等待整个页面重新加载,速度更快,用户体验更佳。
- 性能: 减少了服务器请求和数据传输量(后续页面通常只加载必要的数据)。
- 开发体验: 可以在一个统一的框架下管理整个应用的状态和逻辑。
然而,纯粹的 SPA 也会带来一些问题:
- 如何区分不同的“页面”? 我们的应用只有一个 HTML 文件,如何让
myapp.com/about
显示“关于”内容,而myapp.com/contact
显示“联系我们”内容? - 如何使用浏览器的前进/后退按钮? 如果没有路由,每次内容更新都会覆盖历史记录,用户无法导航。
- 如何分享特定页面的链接? 如果 URL 不变,用户无法收藏或分享应用内部某个具体内容的链接。
客户端路由库(如 React Router)通过监听 URL 的变化(通常使用浏览器的 History API 或 Hashchange 事件),然后在客户端根据 URL 匹配并渲染相应的 React 组件,巧妙地解决了这些问题。它更新 URL,但阻止浏览器的默认完全刷新行为。
React Router v6 的核心理念与优势
React Router v6 是 React Router 的最新主要版本,相较于 v5 引入了一些显著的变化和改进,使得路由的配置更加简洁和直观。其核心理念包括:
- 组件化: React Router 的所有功能都是通过组件来实现的 (
<BrowserRouter>
,<Routes>
,<Route>
,<Link>
),这与 React 本身的组件化思想完美契合。你可以像构建其他 UI 组件一样构建你的路由结构。 - 声明式: 你不是命令式地告诉 React Router “当 URL 是
/about
时,加载 About 组件”,而是声明“如果 URL 匹配/about
,就渲染<About />
组件”。这种声明式风格与 React 的渲染方式一致。 - Hooks 优先: v6 提供了强大的 Hooks (
useParams
,useNavigate
,useLocation
,useResolvedPath
,useMatch
),让你在函数组件中能够方便地访问路由信息和进行导航操作。 - 嵌套路由更自然: v6 改进了嵌套路由的处理方式,通过
<Outlet />
组件使得父子路由之间的协作更加清晰。 - 更小的包体积: v6 在内部进行了优化,提供了更小的构建体积。
入门前的准备
在开始之前,请确保你已经安装了 Node.js、npm 或 yarn,并且对 React 的基础知识(组件、JSX、Props、State、Hooks)有一定的了解。
我们将使用 create-react-app
或 Vite 创建一个基本的 React 项目作为起点。如果你还没有项目,可以这样快速创建一个:
使用 create-react-app
:
bash
npx create-react-app my-react-router-app
cd my-react-router-app
使用 Vite (推荐,更轻量快速):
bash
npm create vite@latest my-react-router-app --template react
cd my-react-router-app
npm install
创建项目后,你需要安装 React Router:
“`bash
npm install react-router-dom
或者
yarn add react-router-dom
“`
react-router-dom
是专门用于 Web 应用的 React Router 版本,它包含了 react-router
的核心功能以及用于 DOM 环境的特定组件(如 BrowserRouter
, Link
)。
核心组件详解与基本使用
React Router v6 最常用的核心组件包括:
<BrowserRouter>
: 路由的容器。它利用浏览器的 History API (pushState
,replaceState
,popstate
事件) 来保持 UI 和 URL 的同步。这是在大多数 Web 应用中最常用的路由器。你需要将你的整个应用(或至少是需要路由的部分)包裹在其中。<Routes>
: v6 中引入的新组件,取代了 v5 的<Switch>
。它用于包裹一组<Route>
组件。<Routes>
会遍历其所有的子<Route>
,并渲染第一个匹配当前 URL 的<Route>
。<Route>
: 定义单个路由。它有两个主要的 props:path
: 要匹配的 URL 路径。element
: 当path
匹配成功时,要渲染的 React 元素 (JSX)。
<Link>
: 用于创建导航链接。它会渲染一个<a>
标签,但会阻止默认的页面刷新行为,而是通过 History API 更新 URL,并通知<BrowserRouter>
发生变化,从而触发路由匹配和组件渲染。其主要 props 是to
,指定要跳转的目标路径。
让我们来构建一个非常简单的应用,包含 Home 和 About 两个页面。
首先,创建两个简单的组件文件(例如在 src/pages/
目录下):
src/pages/Home.js
:
“`javascript
import React from ‘react’;
function Home() {
return (
首页
欢迎来到我们的网站!
);
}
export default Home;
“`
src/pages/About.js
:
“`javascript
import React from ‘react’;
function About() {
return (
关于我们
这是关于我们的页面。
);
}
export default About;
“`
接下来,修改 src/App.js
来集成 React Router:
src/App.js
:
“`javascript
import React from ‘react’;
import { BrowserRouter, Routes, Route, Link } from ‘react-router-dom’; // 导入核心组件
// 导入页面组件
import Home from ‘./pages/Home’;
import About from ‘./pages/About’;
function App() {
return (
// 1. 使用 BrowserRouter 包裹整个需要路由的应用部分
<hr /> {/* 分隔符 */}
{/* 2. 使用 Routes 包裹所有 Route 定义 */}
<Routes>
{/* 3. 定义 Route:path="/" 匹配根路径,渲染 Home 组件 */}
<Route path="/" element={<Home />} />
{/* 定义 Route:path="/about" 匹配 /about 路径,渲染 About 组件 */}
<Route path="/about" element={<About />} />
</Routes>
</div>
</BrowserRouter>
);
}
export default App;
“`
在 src/index.js
(如果你使用的是 create-react-app
) 或 src/main.jsx
(如果你使用的是 Vite) 中,确保 App
组件被渲染即可,通常不需要额外修改,因为 <BrowserRouter>
已经包裹在 App.js
里了。
现在,运行你的应用 (npm start
或 yarn dev
)。你会看到页面上有一个导航菜单和下方的内容区域。点击“首页”链接,URL 会变为 /
,下方显示 Home 组件的内容。点击“关于我们”链接,URL 变为 /about
,下方显示 About 组件的内容。尝试使用浏览器的前进/后退按钮,你会发现它们按预期工作。
这就是 React Router 最基本的用法:包裹 -> 定义路由容器 -> 定义具体路由 -> 创建链接。
深入理解 <Route>
的匹配
在 React Router v6 中,<Routes>
会找到第一个匹配当前 URL 的 <Route>
并渲染其 element
。
- 精确匹配 (Exact Matching): 在 v5 中,根路径
/
默认会匹配所有以/
开头的路径(如/about
,/contact
),需要exact
prop 来实现精确匹配。但在 v6 中,匹配逻辑更加智能,<Route path="/">
只会精确匹配根路径/
。如果你想要匹配以/
开头的所有路径,可以使用通配符/*
(尽管在<Routes>
内部通常不需要这样做,因为<Routes>
只渲染第一个匹配项)。 - 匹配优先级:
<Routes>
从上往下查找匹配项。因此,更具体的路径应该放在前面,例如/users/me
应该放在/users/:id
前面,以确保/users/me
被优先匹配。
构建更复杂的导航:嵌套路由
许多应用有嵌套的结构,比如一个 Dashboard 区域,里面有 Profile、Settings 等子页面,并且这些子页面可能共享一个侧边栏或顶栏布局。React Router v6 提供了强大的嵌套路由支持,通过 <Outlet />
组件实现。
思路是:父路由负责渲染共享的布局部分和一个占位符 (<Outlet />
),子路由则负责填充这个占位符中的内容。
我们来添加一个 Dashboard 区域,包含 Dashboard 主页和 Settings 子页面。
创建组件:
src/pages/Dashboard.js
: (父组件,包含共享布局和 <Outlet>
)
“`javascript
import React from ‘react’;
import { Link, Outlet } from ‘react-router-dom’; // 导入 Outlet
function Dashboard() {
return (
仪表盘
<hr />
{/* Outlet 会渲染匹配到的子路由的 element */}
<div>
<h3>Dashboard Content:</h3>
<Outlet />
</div>
</div>
);
}
export default Dashboard;
“`
src/pages/DashboardHome.js
: (Dashboard 的主页内容)
“`javascript
import React from ‘react’;
function DashboardHome() {
return (
欢迎来到仪表盘主页!
{/ 这里可以放置仪表盘的概览内容 /}
);
}
export default DashboardHome;
“`
src/pages/DashboardSettings.js
: (Settings 子页面内容)
“`javascript
import React from ‘react’;
function DashboardSettings() {
return (
这是仪表盘的设置页面。
{/ 这里是设置表单等内容 /}
);
}
export default DashboardSettings;
“`
修改 src/App.js
添加 Dashboard 路由和其嵌套路由:
src/App.js
:
“`javascript
import React from ‘react’;
import { BrowserRouter, Routes, Route, Link } from ‘react-router-dom’;
// 导入页面组件
import Home from ‘./pages/Home’;
import About from ‘./pages/About’;
// 导入 Dashboard 相关的组件
import Dashboard from ‘./pages/Dashboard’;
import DashboardHome from ‘./pages/DashboardHome’; // 需要一个默认子路由的内容
import DashboardSettings from ‘./pages/DashboardSettings’;
function App() {
return (
<hr />
<Routes>
{/* 基本路由 */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
{/* 定义 Dashboard 父路由 */}
{/* path="/dashboard" 匹配 /dashboard 及其子路径 */}
{/* element={<Dashboard />} 渲染 Dashboard 组件,它包含 <Outlet /> */}
<Route path="/dashboard" element={<Dashboard />}>
{/* 定义嵌套子路由 */}
{/* index 路由:匹配父路由 path="/dashboard" 精确路径时渲染,作为默认内容 */}
{/* path="settings" 匹配 /dashboard/settings */}
<Route index element={<DashboardHome />} /> {/* 匹配 /dashboard */}
<Route path="settings" element={<DashboardSettings />} /> {/* 匹配 /dashboard/settings */}
{/* 可以在这里添加更多子路由,例如 <Route path="profile" element={<DashboardProfile />} /> */}
</Route>
</Routes>
</div>
</BrowserRouter>
);
}
export default App;
“`
注意嵌套路由的定义方式:我们在父 <Route path="/dashboard" element={<Dashboard />}>
内部定义子 <Route>
。
- 子路由的
path
可以是相对于父路由的 ("settings"
会匹配/dashboard/settings
) 或绝对路径 ("/dashboard/settings"
,但通常使用相对路径更灵活)。 index
路由 (<Route index element={<DashboardHome />} />
) 是一个特殊的子路由,它没有path
,当父路由的path
匹配时,它会被渲染到父路由的<Outlet />
中,通常用作父路由的默认内容(例如访问/dashboard
时显示 Dashboard 主页)。
现在,访问 /dashboard
会渲染 Dashboard
组件,并在 <Outlet />
位置渲染 DashboardHome
。点击“设置”,URL 变为 /dashboard/settings
,<Outlet />
位置的内容会切换为 DashboardSettings
,而 Dashboard
组件的导航和布局部分会保持不变。
处理动态 URL 参数:useParams
很多时候,你的 URL 中会包含动态的部分,例如用户 ID (/users/123
) 或产品 Slug (/products/react-book
)。React Router 允许你在 path
定义中使用冒号 :
来标记这些动态参数。例如,/users/:userId
或 /products/:productSlug
.
要获取这些动态参数的值,在函数组件中可以使用 useParams
hook。
创建一个 UserDetail 组件:
src/pages/UserDetail.js
:
“`javascript
import React from ‘react’;
import { useParams } from ‘react-router-dom’; // 导入 useParams
function UserDetail() {
// 使用 useParams 获取 URL 中的动态参数
// 返回一个对象,键是你在 Route path 中定义的参数名 (例如 :userId),值是对应的 URL 部分
const { userId } = useParams();
// 通常你会根据 userId 去请求用户数据
const [user, setUser] = React.useState(null);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
// 模拟数据加载
setTimeout(() => {
// 实际应用中这里会是 API 调用
const dummyUsers = {
‘1’: { id: 1, name: ‘Alice’ },
‘2’: { id: 2, name: ‘Bob’ },
};
setUser(dummyUsers[userId]);
setLoading(false);
}, 500);
}, [userId]); // 当 userId 变化时重新加载数据
if (loading) {
return
;
}
if (!user) {
return
;
}
return (
用户详情
用户 ID: {userId}
用户名: {user.name}
{/ 显示更多用户详情 /}
);
}
export default UserDetail;
“`
在 src/App.js
中添加这个动态路由:
“`javascript
// … 其他导入
// 导入 UserDetail
import UserDetail from ‘./pages/UserDetail’;
function App() {
return (
<hr />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />}>
<Route index element={<DashboardHome />} />
<Route path="settings" element={<DashboardSettings />} />
</Route>
{/* 定义动态路由 */}
{/* :userId 是一个动态参数,它的值将可以在 useParams 中获取 */}
<Route path="/users/:userId" element={<UserDetail />} />
</Routes>
</div>
</BrowserRouter>
);
}
export default App;
“`
现在,当你点击“用户详情 (ID 1)”时,URL 变为 /users/1
,UserDetail
组件会渲染,并通过 useParams()
获取到 { userId: '1' }
。点击“用户详情 (ID 2)”时,URL 变为 /users/2
,useParams()
获取到 { userId: '2' }
,组件会根据新的 userId
更新显示内容。
程序化导航:useNavigate
并非所有的导航都来自用户的点击 <Link>
。有时你需要在代码中进行页面跳转,例如:
- 用户成功登录后跳转到仪表盘。
- 表单提交成功后跳转到详情页。
- 点击按钮触发特定操作后跳转。
这时可以使用 useNavigate
hook。它返回一个 navigate
函数,调用这个函数就可以进行导航。
修改 Home 组件,添加一个跳转到 About 页面的按钮:
src/pages/Home.js
:
“`javascript
import React from ‘react’;
import { useNavigate } from ‘react-router-dom’; // 导入 useNavigate
function Home() {
const navigate = useNavigate(); // 调用 hook 获取导航函数
const handleNavigateToAbout = () => {
// 调用 navigate 函数进行导航
// 参数是要跳转的路径
navigate(‘/about’);
// navigate 也支持传入一个数字,例如 navigate(-1) 返回上一页,navigate(1) 前进一页
// navigate('/', { replace: true }); // replace: true 相当于 history.replaceState,替换当前历史记录而不是新增
};
return (
首页
欢迎来到我们的网站!
);
}
export default Home;
“`
现在,当你访问首页并点击按钮时,应用会通过代码跳转到 /about
路径。
处理未匹配的路由:404 页面
如果用户访问了一个你的应用中没有定义的 URL,你通常希望显示一个“404 – Not Found”页面。在 React Router v6 中,你可以使用 path="*"
来匹配任何未被前面 <Route>
匹配的路径。这个“捕获所有”的路由应该放在 <Routes>
的最后。
创建一个 NotFound 组件:
src/pages/NotFound.js
:
“`javascript
import React from ‘react’;
import { useLocation } from ‘react-router-dom’; // 可以使用 useLocation 获取当前路径
function NotFound() {
const location = useLocation(); // 获取当前位置信息
return (
404 – 页面未找到
您尝试访问的路径 {location.pathname}
不存在。
{/ 可以添加一个链接回到首页 /}
回到首页
);
}
export default NotFound;
“`
在 src/App.js
中添加这个路由:
“`javascript
import React from ‘react’;
import { BrowserRouter, Routes, Route, Link } from ‘react-router-dom’;
// … 其他导入
import NotFound from ‘./pages/NotFound’; // 导入 NotFound
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />}>
<Route index element={<DashboardHome />} />
<Route path="settings" element={<DashboardSettings />} />
</Route>
<Route path="/users/:userId" element={<UserDetail />} />
{/* 404 Route - 放在最后 */}
{/* path="*" 会匹配任何未被上面 Route 匹配的路径 */}
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</BrowserRouter>
);
}
export default App;
“`
现在,访问 /abcde
或 /non-existent-page
等任何未匹配的路径,都会显示 404 页面。
重定向:<Navigate>
组件
有时你需要将用户从一个路径自动重定向到另一个路径。例如,访问旧的 URL 需要跳转到新的 URL,或者在用户未登录时尝试访问需要认证的页面,将其重定向到登录页。
在 React Router v6 中,推荐使用 <Navigate>
组件进行声明式重定向。<Navigate>
组件渲染时就会触发导航,它取代了 v5 中的 <Redirect>
。
例如,假设你想将访问 /old-about
的用户重定向到 /about
:
在 src/App.js
的 <Routes>
中添加:
“`javascript
import React from ‘react’;
import { BrowserRouter, Routes, Route, Link, Navigate } from ‘react-router-dom’; // 导入 Navigate
// … 其他导入
function App() {
return (
<Routes>
{/* ... 其他 Route */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />}>
<Route index element={<DashboardHome />} />
<Route path="settings" element={<DashboardSettings />} />
</Route>
<Route path="/users/:userId" element={<UserDetail />} />
{/* 重定向示例 */}
{/* 当路径匹配 "/old-about" 时,不渲染任何组件,而是直接导航到 "/about" */}
<Route path="/old-about" element={<Navigate to="/about" replace />} /> {/* replace prop 类似 navigate({ replace: true }) */}
{/* 404 Route */}
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</BrowserRouter>
);
}
export default App;
“`
现在,当你访问 /old-about
或点击“旧关于页面”链接时,URL 会自动变为 /about
,并显示 About 页面内容。
你也可以在组件内部根据条件进行重定向:
“`javascript
// 在某个组件内部
import React from ‘react’;
import { Navigate } from ‘react-router-dom’;
function ProtectedComponent({ isAuthenticated }) {
if (!isAuthenticated) {
// 如果用户未认证,渲染 Navigate 组件进行重定向
return
}
// 否则,渲染受保护的内容
return (
);
}
export default ProtectedComponent;
“`
然后在 <Routes>
中使用这个组件:
“`javascript
// … 在 App.js 或其他地方
// 假设你有某种方式获取用户认证状态
const [isAuthenticated, setIsAuthenticated] = React.useState(false); // 示例状态
// … 在 Routes 内部
“`
更多有用的 Hooks
React Router v6 提供了一些其他实用的 Hooks:
useLocation
: 返回当前的location
对象,包含pathname
(当前 URL 路径),search
(URL 中的查询字符串),hash
(URL 中的哈希值) 等信息。这对于获取当前 URL 的详细信息或在 URL 变化时触发副作用很有用(例如数据跟踪)。useSearchParams
: 用于方便地读取和修改 URL 中的查询字符串参数 (query parameters)。它返回一个包含查询参数的对象和一个用于更新它们的函数。
示例 useLocation
:
“`javascript
// 在任何函数组件中
import { useLocation } from ‘react-router-dom’;
function MyComponent() {
const location = useLocation();
console.log(location.pathname); // 例如: “/about”
console.log(location.search); // 例如: “?id=123&name=test”
console.log(location.hash); // 例如: “#section”
return (
);
}
“`
示例 useSearchParams
:
“`javascript
// 在任何函数组件中
import { useSearchParams } from ‘react-router-dom’;
function SearchResultsPage() {
// 返回 [searchParams, setSearchParams]
// searchParams 是一个 URLSearchParams 实例
const [searchParams, setSearchParams] = useSearchParams();
// 获取特定参数的值
const query = searchParams.get(‘query’);
const page = searchParams.get(‘page’) || ‘1’; // 提供默认值
// … 根据 query 和 page 加载数据
// 更新参数
const handlePageChange = (newPage) => {
// setSearchParams 可以接受一个对象或函数
setSearchParams({ query, page: newPage }); // URL 将更新为 ?query=…&page=…
};
return (
搜索结果
搜索关键词: {query}
当前页: {page}
);
}
“`
其他 Router 类型 (简要了解)
除了 <BrowserRouter>
,react-router-dom
还提供了其他类型的路由器,但在大多数 Web 应用中 <BrowserRouter>
是首选:
<HashRouter>
: 使用 URL 的哈希部分 (#
) 来同步 UI 和 URL(例如myapp.com/#/about
)。不依赖 History API,兼容性更好,但在 SEO 和外观上不如BrowserRouter
。<MemoryRouter>
: 将 URL 历史保存在内存中,不会读取或写入浏览器的地址栏。主要用于测试或非浏览器环境 (如 React Native)。
在入门阶段,专注于掌握 <BrowserRouter>
即可。
总结与下一步
恭喜你!现在你已经掌握了 React Router v6 的核心概念和基本用法:
- 理解了客户端路由的需求和 React Router 的作用。
- 学会了如何安装和设置 React Router。
- 掌握了
<BrowserRouter>
,<Routes>
,<Route>
,<Link>
这四大核心组件的使用。 - 理解了如何定义基本路由和创建导航链接。
- 学会了如何利用
<Outlet>
和嵌套<Route>
构建具有共享布局的复杂页面结构。 - 知道了如何使用
useParams
获取动态 URL 参数。 - 学会了使用
useNavigate
进行程序化导航。 - 了解了如何通过
path="*"
实现 404 页面。 - 掌握了使用
<Navigate>
进行重定向。 - 简要了解了
useLocation
和useSearchParams
等其他实用 Hooks。
这只是 React Router v6 功能的冰山一角。在实践中,你可能会遇到更复杂的场景,例如:
- 代码分割和懒加载 (Lazy Loading): 如何只在访问某个路由时才加载对应的组件代码,以优化应用性能?React 的
React.lazy()
和Suspense
可以与 React Router 结合使用。 - 数据加载 (Data Loading): 在渲染某个路由组件之前,如何预先加载它所需的数据?v6.4+ 引入了数据加载 API。
- 认证和授权: 如何保护某些路由,只允许特定用户访问?通常通过自定义路由包装组件或使用 Loader/Action 实现。
- 滚动恢复 (Scroll Restoration): 如何在页面切换时记住用户的滚动位置?
要深入学习这些高级主题,强烈建议查阅 React Router 官方文档。官方文档是最新、最权威的学习资源,包含详细的 API 参考、指南和示例。
现在,就开始在你自己的 React 项目中实践 React Router 吧!尝试将一个已有的多页应用重构成一个 SPA,或者从零开始构建一个具有复杂导航结构的单页应用。通过动手实践,你将更快地掌握 React Router 的强大功能。
祝你学习愉快!