从零开始学习 React Router DOM:React Router DOM 基础
欢迎来到单页应用(Single Page Application, SPA)的世界!在现代前端开发中,SPA 已经成为主流。与传统的多页应用不同,SPA 只需要加载一次 HTML、CSS 和 JavaScript,然后通过 JavaScript 动态地更新页面内容,从而提供更流畅、更接近原生应用的体验。
然而,SPA 带来了一个新的挑战:如何管理前端路由? 在多页应用中,页面的切换依赖于浏览器地址栏的变化和服务器的响应。但在 SPA 中,地址栏的变化并不会触发页面的完全刷新,我们需要一种机制来根据 URL 的变化,在不重新加载整个页面的情况下,渲染不同的组件或视图。
这就是前端路由库的用武之地。对于 React 应用来说,最流行、最强大的前端路由解决方案莫过于 React Router DOM 。它提供了一套丰富的组件和钩子(Hooks),帮助我们在 React 应用中实现声明式路由。
本篇文章将带你从零开始,深入理解 React Router DOM 的基础知识,包括它的核心概念、如何安装、如何配置,以及最常用的组件和钩子。学完本文,你将能够轻松地在自己的 React 应用中实现基本的页面导航和路由管理。
为什么选择 React Router DOM?
React Router DOM 是 React Router 家族的一部分,专门用于 Web 应用程序。它有几个显著的优点:
声明式路由: React Router DOM 鼓励使用声明式的方式定义路由,就像你编写 React 组件一样。这使得路由配置更加直观和易于维护。
与 React 生态紧密集成: 作为 React 生态的一部分,React Router DOM 与 React 的组件生命周期、状态管理等机制结合得非常好。
功能强大且灵活: 它支持嵌套路由、URL 参数、编程式导航、路由守卫(虽然基础篇不深入,但它是重要的扩展功能)等多种高级功能。
社区活跃,文档丰富: 遇到问题时,很容易找到解决方案和帮助。
好了,废话不多说,让我们开始学习吧!
第一步:安装 React Router DOM
在开始使用 React Router DOM 之前,你需要将其安装到你的 React 项目中。打开你的项目终端,运行以下命令之一:
使用 npm:
bash
npm install react-router-dom
使用 yarn:
bash
yarn add react-router-dom
安装完成后,你就可以在你的项目中导入并使用 React Router DOM 提供的组件和钩子了。
第二步:核心概念与基础组件
React Router DOM 的基础主要围绕以下几个核心概念和对应的组件:
路由器 (Routers): 这是路由功能的入口,你需要用一个路由器组件包裹你的整个应用(或者至少是需要路由的部分)。它负责监听 URL 的变化,并根据配置匹配相应的路由。
路由容器 (Routes): 这是一个新的组件(v6+ 版本引入),用于包裹所有的 Route
组件。它会遍历其子元素 Route
,并渲染第一个 匹配当前 URL 的 Route
。
路由 (Route): 定义了特定的 URL 路径应该渲染哪个组件。
链接 (Links): 用于在应用内部进行导航,避免浏览器整页刷新。
渲染内容出口 (Outlet): 在嵌套路由中,用于标记子路由应该渲染在父组件的哪个位置(v6+ 版本)。
钩子 (Hooks): 用于在函数组件中获取路由信息或进行编程式导航。
useParams
useNavigate
useLocation
(基础篇暂不详细展开,但它是常用的)
接下来,我们将逐一详细介绍这些组件和钩子。
2.1 路由器 (Routers): BrowserRouter
vs HashRouter
你需要将你的应用的根组件(通常是 App
或 Index
)包裹在一个路由器组件中。常用的有两种:
BrowserRouter
: 使用 HTML5 History API (pushState
, replaceState
, popstate
) 来保持 UI 与 URL 的同步。它创建了“干净”的 URL,例如 http://localhost:3000/about
。这是构建现代 Web 应用推荐的方式,因为它更符合用户习惯和 SEO 友好。但需要服务器端进行配置,将所有路由请求都重定向到你的应用入口文件(通常是 index.html
),以便前端路由能够接管。
HashRouter
: 使用 URL 的哈希部分 (#
) 来保持 UI 与 URL 的同步。例如 http://localhost:3000/#/about
。哈希路由的优点是无需服务器端特殊配置,因为 #
后面的内容不会发送到服务器。缺点是 URL 不太美观,且对 SEO 不太友好。适合用于静态网站或无需服务器端配置的简单应用。
在绝大多数情况下,你会选择 BrowserRouter
。我们将以 BrowserRouter
为例进行讲解。
使用示例:
在你的应用入口文件(例如 src/index.js
或 src/main.jsx
):
“`javascript
import React from ‘react’;
import ReactDOM from ‘react-dom/client’; // 或者 ‘react-dom’
import { BrowserRouter } from ‘react-router-dom’;
import App from ‘./App’;
import ‘./index.css’;
const root = ReactDOM.createRoot(document.getElementById(‘root’));
root.render(
{/ 用 BrowserRouter 包裹你的应用根组件 /}
);
“`
或者在你的 App.js
文件中包裹主要内容:
“`javascript
// App.js
import React from ‘react’;
import { BrowserRouter, Routes, Route, Link } from ‘react-router-dom’;
import Home from ‘./pages/Home’;
import About from ‘./pages/About’;
import Contact from ‘./pages/Contact’;
function App() {
return (
{/ 也可以在这里包裹,但通常推荐在入口文件包裹 /}
{/ /}
{/* Routes 是路由的容器 */}
<Routes>
{/* Route 定义具体的路由规则 */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</div>
{/* </BrowserRouter> */}
);
}
export default App;
“`
重要提示: 一个应用通常只需要一个路由器。将路由器放在应用的最顶层,以便整个组件树都可以访问到路由上下文。
2.2 路由容器 (Routes) 和 路由 (Route)
Routes
是 React Router v6+ 版本引入的,取代了之前的 Switch
。它的主要作用是作为一个容器,里面包含了一系列的 Route
组件。当 URL 变化时,Routes
会遍历它的子 Route
,并渲染第一个 路径与当前 URL 匹配的 Route
所对应的 element
。
Route
组件定义了具体的路由规则。它至少需要两个重要的属性:
path
: 一个字符串,定义了该路由匹配的 URL 路径。例如 /
, /about
, /users/:userId
。
element
: 一个 React 元素(通常是一个组件实例,即 <MyComponent />
),当该路由匹配时,这个元素会被渲染。
使用示例:
“`javascript
// 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 (
// … 导航栏或其他内容 …
{/ 当 URL 是 “/” 时,渲染 Home 组件 /}
} />
{/* 当 URL 是 "/about" 时,渲染 About 组件 */}
<Route path="/about" element={<About />} />
{/* 当 URL 是 "/contact" 时,渲染 Contact 组件 */}
<Route path="/contact" element={<Contact />} />
{/* 可以定义一个通配符路由来处理未匹配到的路径(404页面) */}
{/* 注意:通配符路由通常放在 Routes 的最后 */}
<Route path="*" element={<div>404 - Page Not Found</div>} />
</Routes>
// ...
);
}
“`
Routes
的特点 (v6+):
它只会渲染第一个匹配的 Route
。这意味着你不再需要 exact
属性来确保精确匹配,因为 /
不会匹配 /about
。这是与 Switch
的一个重要区别。
支持相对路径和嵌套路由的定义(稍后会详细介绍)。
2.3 链接 (Links): Link
vs NavLink
在 SPA 中,我们不能使用普通的 <a>
标签来进行应用内的导航,因为 <a>
标签的点击会触发浏览器的整页刷新,这会破坏 SPA 的体验。React Router DOM 提供了 Link
和 NavLink
组件来解决这个问题。
Link
: 一个基本的导航组件,用于在应用内部切换路由。它会渲染成一个带有正确 href
属性的 <a>
标签,但会阻止默认的浏览器跳转行为,转而使用 History API 或哈希变化来更新 URL 并触发路由匹配。
NavLink
: 是 Link
的一个特殊版本。除了具有 Link
的功能外,它还能在你当前所在的路由与 NavLink
的 to
属性匹配时,自动为其添加一个激活状态的 class(默认为 active
)。这非常有用,可以方便地高亮显示当前页面对应的导航链接。
使用示例:
“`javascript
// App.js 的一部分 或 导航组件中
import { Link, NavLink } from ‘react-router-dom’;
function Navigation() {
return (
{/ 使用 Link 进行基本导航 /}
首页
关于我们
联系我们
{/* 使用 NavLink,当前路径匹配时会添加 'active' class */}
<li>
<NavLink to="/" activeClassName="active">首页 (NavLink)</NavLink> {/* activeClassName 已弃用,见下文 */}
</li>
<li>
<NavLink to="/about" activeClassName="active">关于我们 (NavLink)</NavLink>
</li>
</ul>
</nav>
);
}
“`
NavLink
的激活样式 (v6+ 更新):
在 React Router v6+ 中,activeClassName
和 activeStyle
属性已被移除。现在你可以通过 className
和 style
属性接收一个函数来实现动态样式。该函数会接收一个对象,其中包含一个 isActive
布尔值,指示当前链接是否处于激活状态。
v6+ NavLink
示例:
“`javascript
import { NavLink } from ‘react-router-dom’;
function Navigation() {
return (
isActive ? ‘link active’ : ‘link’}
style={({ isActive }) => ({ color: isActive ? ‘red’ : ‘black’ })}
>
首页
isActive ? ‘link active’ : ‘link’}
>
关于我们
);
}
“`
你可以通过 CSS 定义 .link.active
的样式,或者直接在 style
函数中返回样式对象。
第三步:一个简单的 React Router DOM 示例
让我们把上面学到的基础知识串起来,构建一个简单的多页应用。
假设我们的应用有三个页面:首页、关于我们和联系我们。
创建页面组件:
创建三个简单的函数组件文件:src/pages/Home.js
, src/pages/About.js
, src/pages/Contact.js
。
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/pages/Contact.js
:
“`javascript
import React from ‘react’;
function Contact() {
return (
);
}
export default Contact;
“`
配置路由和导航:
修改 src/App.js
文件来设置路由和导航链接。
src/App.js
:
“`javascript
import React from ‘react’;
import { BrowserRouter as Router, 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’; // 如果你想添加一些样式
function App() {
return (
{/ 或者在 index.js 中包裹 /}
{/
导航栏 /}
<hr /> {/* 添加一条分隔线 */}
{/* 路由配置区域 */}
<div className="page-content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
“`
确保应用被路由器包裹:
如果按照上面 index.js
的示例,已经在入口文件包裹了 BrowserRouter
,那么 App.js
中的 <Router>
(在这里我们为了清晰使用了 BrowserRouter as Router
,但直接用 BrowserRouter
也可以)可以去掉,避免重复包裹。
src/index.js
(推荐方式):
“`javascript
import React from ‘react’;
import ReactDOM from ‘react-dom/client’;
import { BrowserRouter } from ‘react-router-dom’; // 导入 BrowserRouter
import App from ‘./App’;
import ‘./index.css’;
const root = ReactDOM.createRoot(document.getElementById(‘root’));
root.render(
{/ 在应用的最外层包裹 BrowserRouter /}
{/ App 组件中不再需要包裹 BrowserRouter /}
);
“`
修改后的 src/App.js
(如果已在 index.js
中包裹):
“`javascript
import React from ‘react’;
import { Routes, Route, Link } from ‘react-router-dom’; // 不再需要 BrowserRouter
import Home from ‘./pages/Home’;
import About from ‘./pages/About’;
import Contact from ‘./pages/Contact’;
import ‘./App.css’;
function App() {
return (
{/
导航栏 /}
<hr />
{/* 路由配置区域 */}
<div className="page-content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</div>
</div>
);
}
export default App;
“`
现在运行你的应用 (npm start
或 yarn start
),你将看到一个简单的导航栏。点击导航链接,下方的页面内容区域将根据 URL 的变化动态地显示不同的页面组件,而无需整页刷新。
第四步:进阶基础概念
掌握了上面的核心组件,你已经可以在 React 应用中实现基本的页面导航了。但实际应用往往更复杂,我们需要处理一些更高级的场景。React Router DOM 提供了一些额外的功能来应对这些挑战。
4.1 嵌套路由 (Nested Routes)
在大型应用中,经常会遇到嵌套的界面结构,比如用户 Dashboard 页面,内部可能有“个人资料”、“设置”、“订单历史”等子页面。URL 结构可能像这样:/dashboard/profile
, /dashboard/settings
, /dashboard/orders
。
React Router DOM 非常优雅地支持嵌套路由。实现嵌套路由主要通过以下方式:
在父 Route
中定义相对路径的子 Route
: 在父 Route
的 path
后加上 /
并定义子路径。
在父组件中使用 Outlet
: 父路由匹配时渲染父组件,父组件内部的 <Outlet />
组件会作为子路由渲染内容的“出口”。
示例:用户 Dashboard
假设我们有一个 /dashboard
路由,它内部包含 /dashboard/profile
和 /dashboard/settings
。
创建组件:
src/pages/Dashboard.js
(父组件,包含导航和 Outlet
)
src/pages/DashboardIndex.js
(Dashboard 的默认页面,/dashboard
)
src/pages/Profile.js
(个人资料页面,/dashboard/profile
)
src/pages/Settings.js
(设置页面,/dashboard/settings
)
src/pages/Dashboard.js
:
“`javascript
import React from ‘react’;
import { Outlet, Link } from ‘react-router-dom’; // 导入 Outlet 和 Link
function Dashboard() {
return (
用户 Dashboard
<hr />
{/* Outlet 是子路由渲染的位置 */}
<Outlet />
</div>
);
}
export default Dashboard;
“`
src/pages/DashboardIndex.js
:
“`javascript
import React from ‘react’;
function DashboardIndex() {
return (
欢迎来到 Dashboard 首页
请选择左侧菜单进行操作。
);
}
export default DashboardIndex;
“`
src/pages/Profile.js
:
“`javascript
import React from ‘react’;
function Profile() {
return (
);
}
export default Profile;
“`
src/pages/Settings.js
:
“`javascript
import React from ‘react’;
function Settings() {
return (
);
}
export default Settings;
“`
在 Routes
中配置嵌套路由:
修改 src/App.js
,在 /dashboard
的 Route
中嵌套子 Route
。
“`javascript
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 Dashboard from ‘./pages/Dashboard’; // 导入 Dashboard 父组件
import DashboardIndex from ‘./pages/DashboardIndex’; // 导入 Dashboard 默认页
import Profile from ‘./pages/Profile’; // 导入子页面组件
import Settings from ‘./pages/Settings’; // 导入子页面组件
// … 其他导入 …
function App() {
return (
{/ … 导航链接 … /}
} />
} />
} />
{/* 定义父路由 */}
<Route path="/dashboard" element={<Dashboard />}>
{/* 定义子路由 */}
{/* 当路径是 "/dashboard" 时,渲染 DashboardIndex。这是父路由的默认内容。 */}
<Route index element={<DashboardIndex />} />
{/* 当路径是 "/dashboard/profile" 时,渲染 Profile */}
<Route path="profile" element={<Profile />} /> {/* 注意:子路由的 path 是相对的 */}
{/* 当路径是 "/dashboard/settings" 时,渲染 Settings */}
<Route path="settings" element={<Settings />} /> {/* 注意:子路由的 path 是相对的 */}
{/* 也可以为嵌套路由定义一个 404 页面 */}
{/* <Route path="*" element={<div>Dashboard - 404 Not Found</div>} /> */}
</Route>
{/* 整个应用的 404 页面 */}
<Route path="*" element={<div>404 - Page Not Found</div>} />
</Routes>
</div>
</div>
);
}
export default App;
“`
通过这种方式,当 URL 匹配 /dashboard
时,Dashboard
组件会被渲染。如果在 /dashboard
后面还有匹配的子路径(如 /dashboard/profile
),那么 Profile
组件将在 Dashboard
组件内部 <Outlet />
的位置渲染。如果没有匹配的子路径但匹配了 /dashboard
本身,如果定义了 <Route index element={...} />
,则会渲染该默认内容。
4.2 URL 参数 (URL Parameters): useParams
很多时候,我们需要从 URL 中获取动态的数据,比如用户 ID、商品 ID 等。例如,/users/123
,我们想获取 123
这个用户 ID。React Router DOM 提供了 useParams
钩子来方便地获取这些参数。
在 Route
的 path
中定义参数: 使用冒号 :
后面跟着参数名的方式定义。例如 /users/:userId
。
在组件中使用 useParams
钩子: 在匹配该路由的组件中,调用 useParams()
钩子可以获取一个对象,其键是你在 path
中定义的参数名,值是 URL 中对应位置的字符串。
示例:用户详情页
假设我们有一个用户详情页,通过 /users/:userId
访问。
创建组件: src/pages/UserProfile.js
src/pages/UserProfile.js
:
“`javascript
import React from ‘react’;
import { useParams } from ‘react-router-dom’; // 导入 useParams
function UserProfile() {
// 调用 useParams() 获取 URL 参数对象
const params = useParams();
const userId = params.userId; // 获取参数名为 ‘userId’ 的值
// 可以在这里根据 userId 从后端或状态管理中获取用户信息
return (
用户详情
当前用户的 ID 是: {userId}
{/ 这里可以展示根据 userId 获取到的用户详细信息 /}
);
}
export default UserProfile;
“`
在 Routes
中配置带参数的路由:
修改 src/App.js
,添加 /users/:userId
路由。
“`javascript
import React from ‘react’;
import { Routes, Route, Link } from ‘react-router-dom’;
// … 其他导入 …
import UserProfile from ‘./pages/UserProfile’; // 导入 UserProfile 组件
function App() {
return (
{/ … 其他链接 … /}
{/ 添加链接,模拟访问不同用户 /}
用户 1
用户 abc
{/ … 其他路由 … /}
{/* 定义带参数的路由 */}
{/* 当 URL 匹配 /users/ 后面的任何内容时,渲染 UserProfile */}
<Route path="/users/:userId" element={<UserProfile />} />
{/* ... 404 路由 ... */}
</Routes>
</div>
</div>
);
}
export default App;
“`
现在,当你访问 /users/123
时,UserProfile
组件会渲染,并且 useParams()
会返回 { userId: '123' }
。你就可以在组件中使用这个 userId
来加载对应用户的数据。
4.3 编程式导航 (Programmatic Navigation): useNavigate
前面我们学习了如何使用 Link
和 NavLink
来实现点击导航。但有时候,导航需要在某些事件发生后通过代码来触发,比如表单提交成功后跳转到详情页,或者点击一个按钮进行页面切换。这时就需要使用编程式导航。
React Router DOM v6+ 提供了 useNavigate
钩子来实现编程式导航。
在组件中调用 useNavigate
钩子: 它会返回一个 navigate
函数。
调用 navigate
函数进行导航: 将目标路径作为参数传递给 navigate
函数。
示例:提交表单后跳转
假设你有一个提交按钮,点击后需要跳转到首页。
“`javascript
import React from ‘react’;
import { useNavigate } from ‘react-router-dom’; // 导入 useNavigate
function MyForm() {
const navigate = useNavigate(); // 调用钩子获取 navigate 函数
const handleSubmit = (event) => {
event.preventDefault(); // 阻止表单默认提交行为
// ... 处理表单数据 ...
// 提交成功后,使用 navigate 函数跳转到首页
navigate('/');
// navigate 函数也支持一些选项
// navigate('/dashboard', { replace: true }); // 替换当前历史记录项
// navigate(-1); // 回退一步
// navigate(1); // 前进一步
};
return (
);
}
export default MyForm;
“`
将 MyForm
组件添加到你的路由配置中,比如在首页 /
渲染它,或者专门为一个表单路径配置一个路由。当用户点击按钮时,handleSubmit
函数会被调用,其中的 navigate('/')
就会将用户导航到首页。
4.4 处理未匹配的路由 (404)
一个健壮的应用应该能处理用户输入了不存在的 URL 的情况,通常是显示一个“404 – 页面未找到”的页面。在 React Router DOM 中,可以通过在 Routes
的最后定义一个带有通配符 *
的 Route
来实现。
*
路径会匹配任何没有被之前的 Route
匹配到的 URL。因为 Routes
会按照顺序匹配并只渲染第一个,所以将 path="*"
的 Route
放在 Routes
的最后,可以确保它只在所有其他路由都不匹配时才生效。
示例:
“`javascript
import React from ‘react’;
import { Routes, Route } from ‘react-router-dom’;
// … 其他导入 …
function App() {
return (
{/
… 导航栏 … /}
} />
} />
} />
} />
} /> {/ 嵌套路由的父路径通常以 / 结尾或不加 */}
{/* 404 页面 - 放在所有其他路由的最后 */}
<Route path="*" element={
<div>
<h1>404 - 页面未找到</h1>
<p>抱歉,您访问的页面不存在。</p>
<Link to="/">返回首页</Link>
</div>
} />
</Routes>
</div>
</div>
);
}
export default App;
“`
第五步:一些重要的使用技巧和注意事项
Router 包裹的位置: 通常建议在应用的入口文件 src/index.js
中包裹,这样整个应用都能访问路由上下文。
Routes
的作用: 它是 V6+ 版本中必须的容器,负责匹配 Route
。记住它只会渲染第一个 匹配项。
Link
vs a
: 在应用内部导航请务必使用 Link
或 NavLink
,而不是原生的 <a>
标签。
相对路径 vs 绝对路径:
Link
的 to
属性:/about
是绝对路径,始终从根路径开始;about
或 ./about
是相对路径,相对于当前 URL。
Route
的 path
属性:在 <Routes>
的顶层,path="/about"
是绝对路径。在嵌套路由中,子 Route
的 path="profile"
是相对路径,会拼接到父 Route
的 path
后面(如果父路径是 /dashboard
,子路径 profile
会匹配 /dashboard/profile
)。
useParams
获取的是字符串: 从 useParams
获取到的参数值总是字符串类型。如果需要数字,记得进行类型转换(例如 parseInt(userId, 10)
)。
服务器端配置 (针对 BrowserRouter
): 如果使用 BrowserRouter
并部署应用,必须配置你的 Web 服务器(如 Nginx, Apache, Express 等),将所有指向你的应用路径的请求都重定向到 index.html
。否则,当用户直接访问非根路径(如 http://yourdomain.com/about
)时,服务器可能会返回 404 错误,因为服务器找不到 /about
文件。这个配置因服务器类型而异,请查阅相应服务器的文档。
总结
恭喜你!通过本文的学习,你已经掌握了 React Router DOM 的基础知识,包括:
认识到 SPA 前端路由的必要性。
学会了如何安装 React Router DOM。
理解并使用了核心组件:BrowserRouter
(或 HashRouter
)、Routes
、Route
、Link
、NavLink
、Outlet
。
学习了如何在应用中构建基本的导航和页面路由。
了解了如何实现嵌套路由。
学会了如何从 URL 中获取动态参数 (useParams
)。
掌握了如何通过代码进行页面跳转 (useNavigate
)。
知道了如何处理 404 页面。
这些是构建任何使用 React 的单页应用所需的最基本的路由知识。
React Router DOM 还有更多强大的功能,比如路由守卫(需要自定义实现或使用第三方库)、useLocation
、useHistory
(v5) / useNavigationType
(v6)、路由配置对象等等。但对于初学者来说,掌握本文涵盖的基础内容已经足够让你开始构建复杂的应用。
下一步:
多动手实践,在你自己的 React 项目中尝试使用这些组件和钩子。
查阅 React Router DOM 官方文档,了解更详细的信息和最新的 API。
探索更高级的主题,如路由守卫、路由配置、测试路由等。
祝你在 React Router DOM 的学习旅程中一切顺利!
Post navigation