从零开始学习 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