路由跳转
本章你将学会用 React Router v6 实现无刷新的页面跳转,包括动态路由和参数读取。
为什么要用路由?
传统的多页面网站,每次点击链接都要向服务器请求新页面,有白屏闪烁。
前端路由 让 URL 变化时由 JS 控制显示哪个组件,不向服务器发请求。
好处:切换流畅,体验像原生 APP。
React Router v6 核心概念
| 概念 | 作用 | 对应代码 |
|---|---|---|
| 路由表 | 定义 URL 与组件的映射 | <Routes> + <Route> |
| 路由出口 | 子路由显示的占位位置 | <Outlet /> |
| 导航链接 | 点击跳转 | <Link to="..."> |
| 动态路由 | URL 中的变量 | /post/:id |
| 读取参数 | 获取动态路由中的值 | useParams() |
安装与配置
$ npm install react-router-dom@6
创建路由配置
实例
// 文件路径:src/router/index.jsx
import { createBrowserRouter } from 'react-router-dom'
import App from '../App'
import HomePage from '../pages/HomePage'
import PostPage from '../pages/PostPage'
// createBrowserRouter 创建路由实例
const router = createBrowserRouter([
{
path: '/', // App 作为根布局
element: <App />,
children: [
{
index: true, // index: true 表示 / 路径的默认子路由
element: <HomePage />
},
{
path: 'post/:id', // 动态路由::id 匹配任意值
element: <PostPage />
}
]
}
])
export default router
import { createBrowserRouter } from 'react-router-dom'
import App from '../App'
import HomePage from '../pages/HomePage'
import PostPage from '../pages/PostPage'
// createBrowserRouter 创建路由实例
const router = createBrowserRouter([
{
path: '/', // App 作为根布局
element: <App />,
children: [
{
index: true, // index: true 表示 / 路径的默认子路由
element: <HomePage />
},
{
path: 'post/:id', // 动态路由::id 匹配任意值
element: <PostPage />
}
]
}
])
export default router
两种路由模式:createBrowserRouter 产生干净的 URL(/post/1),需要服务器配置。如果部署环境不确定,用 createHashRouter,URL 中带 # 号(/#/post/1),无需服务器配置。
在 main.jsx 中注册路由
实例
// 文件路径:src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import router from './router'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
{/* RouterProvider 将路由注入到应用 */}
<RouterProvider router={router} />
</React.StrictMode>
)
import React from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import router from './router'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
{/* RouterProvider 将路由注入到应用 */}
<RouterProvider router={router} />
</React.StrictMode>
)
在 App.jsx 中使用 Outlet
实例
// 文件路径:src/App.jsx
import { Outlet } from 'react-router-dom'
import NavBar from './components/NavBar'
import './App.css'
function App() {
return (
<div className="app">
<NavBar />
<main className="container">
{/* Outlet 是嵌套路由的出口:子路由匹配到的组件会在这里渲染 */}
<Outlet />
</main>
<footer className="footer">
<p>© 2024 RUNOOB Blog. Powered by React.</p>
</footer>
</div>
)
}
export default App
import { Outlet } from 'react-router-dom'
import NavBar from './components/NavBar'
import './App.css'
function App() {
return (
<div className="app">
<NavBar />
<main className="container">
{/* Outlet 是嵌套路由的出口:子路由匹配到的组件会在这里渲染 */}
<Outlet />
</main>
<footer className="footer">
<p>© 2024 RUNOOB Blog. Powered by React.</p>
</footer>
</div>
)
}
export default App
Link — 声明式导航
Link 替代了传统的 <a> 标签,不会触发页面刷新。
实例
import { Link } from 'react-router-dom'
function BlogCard({ id, title, summary, date, category }) {
return (
<Link to={`/post/${id}`} className="card-link">
<div className="card">
<span className="tag">{category}</span>
<h3>{title}</h3>
<p>{summary}</p>
<span className="date">{date}</span>
</div>
</Link>
)
}
function BlogCard({ id, title, summary, date, category }) {
return (
<Link to={`/post/${id}`} className="card-link">
<div className="card">
<span className="tag">{category}</span>
<h3>{title}</h3>
<p>{summary}</p>
<span className="date">{date}</span>
</div>
</Link>
)
}
不要用
<a href="...">——它会导致浏览器向服务器请求新页面,整个 React 应用会重新初始化。用<Link to="...">才能在客户端无刷新切换。
useParams — 读取路由参数
在详情页中,需要从 URL 中取出文章 ID,才能找到对应文章。
实例
import { useParams } from 'react-router-dom'
function PostPage() {
// useParams 返回一个对象,包含 URL 中的参数
const { id } = useParams()
// 注意:id 是字符串,需要数字时可以 Number(id) 转换
return (
<div>
<p>当前文章 ID:{id}</p>
</div>
)
}
function PostPage() {
// useParams 返回一个对象,包含 URL 中的参数
const { id } = useParams()
// 注意:id 是字符串,需要数字时可以 Number(id) 转换
return (
<div>
<p>当前文章 ID:{id}</p>
</div>
)
}
useNavigate — 编程式跳转
某些场景下需要不通过点击链接来跳转(如:表单提交后跳转、未找到文章时跳转回首页)。
实例
import { useNavigate } from 'react-router-dom'
function PostPage() {
const navigate = useNavigate()
const article = null // 假设没找到文章
if (!article) {
// 文章不存在时,跳转回首页
return (
<div>
<p>文章不存在</p>
<button onClick={() => navigate('/')}>返回首页</button>
</div>
)
}
return <div>文章详情...</div>
}
function PostPage() {
const navigate = useNavigate()
const article = null // 假设没找到文章
if (!article) {
// 文章不存在时,跳转回首页
return (
<div>
<p>文章不存在</p>
<button onClick={() => navigate('/')}>返回首页</button>
</div>
)
}
return <div>文章详情...</div>
}
Vue3 vs React 路由 API 对照
| 功能 | Vue3 | React |
|---|---|---|
| 导航链接 | <RouterLink> | <Link> |
| 路由出口 | <RouterView> | <Outlet> |
| 动态路由 | /post/:id | /post/:id |
| 读取参数 | useRoute().params | useParams() |
| 编程式跳转 | useRouter().push() | useNavigate() |
动手:完整的详情页 + 路由整合
第一步:改造 BlogCard 支持点击跳转
实例
// 文件路径:src/components/BlogCard.jsx
import { Link } from 'react-router-dom'
function BlogCard({ id, title, summary, date, category }) {
return (
<Link to={`/post/${id}`} className="card-link">
<div className="card">
<span className="tag">{category}</span>
<h3>{title}</h3>
<p>{summary}</p>
<span className="date">{date}</span>
</div>
</Link>
)
}
export default BlogCard
import { Link } from 'react-router-dom'
function BlogCard({ id, title, summary, date, category }) {
return (
<Link to={`/post/${id}`} className="card-link">
<div className="card">
<span className="tag">{category}</span>
<h3>{title}</h3>
<p>{summary}</p>
<span className="date">{date}</span>
</div>
</Link>
)
}
export default BlogCard
第二步:创建 HomePage 和 PostPage
实例
// 文件路径:src/pages/HomePage.jsx
import { useState, useMemo } from 'react'
import BlogCard from '../components/BlogCard'
import CategoryFilter from '../components/CategoryFilter'
function HomePage() {
const [articles] = useState([
{ id: 1, title: 'React 入门完全指南', summary: '从零学 React', category: 'React', date: '2024-05-10',
content: '<h2>为什么学 React?</h2><p>React 是目前最流行的前端框架之一...</p>' },
{ id: 2, title: 'JS 异步编程详解', summary: '搞懂 Promise 和 async/await', category: 'JavaScript', date: '2024-05-08',
content: '<h2>什么是异步?</h2><p>JS 是单线程的,异步操作可以让主线程不阻塞...</p>' },
{ id: 3, title: 'CSS Grid 布局实战', summary: '用 Grid 实现响应式布局', category: 'CSS', date: '2024-05-05',
content: '<h2>Grid 入门</h2><p>Grid 是二维布局系统...</p>' },
])
const [activeCategory, setActiveCategory] = useState('全部')
const categories = useMemo(() => {
return ['全部', ...new Set(articles.map(a => a.category))]
}, [articles])
const filteredArticles = useMemo(() => {
if (activeCategory === '全部') return articles
return articles.filter(a => a.category === activeCategory)
}, [articles, activeCategory])
return (
<div>
<h2 className="section-title">最新文章</h2>
<CategoryFilter
categories={categories}
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
/>
<p className="result-info">共 {filteredArticles.length} 篇</p>
{filteredArticles.length === 0 ? (
<p className="empty-tip">该分类下暂无文章</p>
) : (
<div className="article-grid">
{filteredArticles.map(article => (
<BlogCard key={article.id} {...article} />
))}
</div>
)}
</div>
)
}
export default HomePage
import { useState, useMemo } from 'react'
import BlogCard from '../components/BlogCard'
import CategoryFilter from '../components/CategoryFilter'
function HomePage() {
const [articles] = useState([
{ id: 1, title: 'React 入门完全指南', summary: '从零学 React', category: 'React', date: '2024-05-10',
content: '<h2>为什么学 React?</h2><p>React 是目前最流行的前端框架之一...</p>' },
{ id: 2, title: 'JS 异步编程详解', summary: '搞懂 Promise 和 async/await', category: 'JavaScript', date: '2024-05-08',
content: '<h2>什么是异步?</h2><p>JS 是单线程的,异步操作可以让主线程不阻塞...</p>' },
{ id: 3, title: 'CSS Grid 布局实战', summary: '用 Grid 实现响应式布局', category: 'CSS', date: '2024-05-05',
content: '<h2>Grid 入门</h2><p>Grid 是二维布局系统...</p>' },
])
const [activeCategory, setActiveCategory] = useState('全部')
const categories = useMemo(() => {
return ['全部', ...new Set(articles.map(a => a.category))]
}, [articles])
const filteredArticles = useMemo(() => {
if (activeCategory === '全部') return articles
return articles.filter(a => a.category === activeCategory)
}, [articles, activeCategory])
return (
<div>
<h2 className="section-title">最新文章</h2>
<CategoryFilter
categories={categories}
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
/>
<p className="result-info">共 {filteredArticles.length} 篇</p>
{filteredArticles.length === 0 ? (
<p className="empty-tip">该分类下暂无文章</p>
) : (
<div className="article-grid">
{filteredArticles.map(article => (
<BlogCard key={article.id} {...article} />
))}
</div>
)}
</div>
)
}
export default HomePage
实例
// 文件路径:src/pages/PostPage.jsx
import { useParams, Link } from 'react-router-dom'
import { useMemo } from 'react'
// 文章数据(后续章节会从外部加载)
const articles = [
{ id: 1, title: 'React 入门完全指南', category: 'React', date: '2024-05-10',
content: '<h2>为什么学 React?</h2><p>React 是目前最流行的前端框架之一...</p>' },
{ id: 2, title: 'JS 异步编程详解', category: 'JavaScript', date: '2024-05-08',
content: '<h2>什么是异步?</h2><p>JS 是单线程的...</p>' },
]
function PostPage() {
const { id } = useParams()
// 根据 ID 查找文章
const article = useMemo(() => {
return articles.find(a => a.id === Number(id))
}, [id])
// 文章不存在
if (!article) {
return (
<div className="not-found">
<h2>文章不存在</h2>
<p>找不到 ID 为 {id} 的文章</p>
<Link to="/">返回首页</Link>
</div>
)
}
// 文章存在
return (
<article className="post-view">
<span className="category-tag">{article.category}</span>
<h1>{article.title}</h1>
<time>{article.date}</time>
{/* dangerouslySetInnerHTML 等价于 Vue 的 v-html,注意 XSS 风险 */}
<div
className="content"
dangerouslySetInnerHTML={{ __html: article.content }}
/>
<Link to="/" className="back-link">← 返回首页</Link>
</article>
)
}
export default PostPage
import { useParams, Link } from 'react-router-dom'
import { useMemo } from 'react'
// 文章数据(后续章节会从外部加载)
const articles = [
{ id: 1, title: 'React 入门完全指南', category: 'React', date: '2024-05-10',
content: '<h2>为什么学 React?</h2><p>React 是目前最流行的前端框架之一...</p>' },
{ id: 2, title: 'JS 异步编程详解', category: 'JavaScript', date: '2024-05-08',
content: '<h2>什么是异步?</h2><p>JS 是单线程的...</p>' },
]
function PostPage() {
const { id } = useParams()
// 根据 ID 查找文章
const article = useMemo(() => {
return articles.find(a => a.id === Number(id))
}, [id])
// 文章不存在
if (!article) {
return (
<div className="not-found">
<h2>文章不存在</h2>
<p>找不到 ID 为 {id} 的文章</p>
<Link to="/">返回首页</Link>
</div>
)
}
// 文章存在
return (
<article className="post-view">
<span className="category-tag">{article.category}</span>
<h1>{article.title}</h1>
<time>{article.date}</time>
{/* dangerouslySetInnerHTML 等价于 Vue 的 v-html,注意 XSS 风险 */}
<div
className="content"
dangerouslySetInnerHTML={{ __html: article.content }}
/>
<Link to="/" className="back-link">← 返回首页</Link>
</article>
)
}
export default PostPage
dangerouslySetInnerHTML是 React 版的v-html。名字特意取得很长,提醒你:不要用它渲染用户输入的内容,有 XSS 安全风险。这里的内容是开发者自己写的,所以是安全的。
添加 PostPage 样式
实例
/* 文件路径:src/App.css 追加详情页样式 */
.post-view {
max-width: 720px;
margin: 0 auto;
}
.category-tag {
display: inline-block;
padding: 4px 12px;
background: #e3f2fd;
color: #1976d2;
border-radius: 12px;
font-size: 13px;
margin-bottom: 12px;
}
.post-view h1 {
font-size: 32px;
margin-bottom: 12px;
line-height: 1.4;
}
.post-view time {
display: block;
color: #999;
font-size: 14px;
margin-bottom: 30px;
}
.content {
line-height: 1.8;
font-size: 16px;
color: #333;
}
.content h2 {
margin: 24px 0 12px;
font-size: 22px;
}
.content p {
margin-bottom: 12px;
}
.back-link {
display: inline-block;
margin-top: 40px;
color: #1976d2;
text-decoration: none;
}
.not-found {
text-align: center;
padding: 60px 0;
}
.post-view {
max-width: 720px;
margin: 0 auto;
}
.category-tag {
display: inline-block;
padding: 4px 12px;
background: #e3f2fd;
color: #1976d2;
border-radius: 12px;
font-size: 13px;
margin-bottom: 12px;
}
.post-view h1 {
font-size: 32px;
margin-bottom: 12px;
line-height: 1.4;
}
.post-view time {
display: block;
color: #999;
font-size: 14px;
margin-bottom: 30px;
}
.content {
line-height: 1.8;
font-size: 16px;
color: #333;
}
.content h2 {
margin: 24px 0 12px;
font-size: 22px;
}
.content p {
margin-bottom: 12px;
}
.back-link {
display: inline-block;
margin-top: 40px;
color: #1976d2;
text-decoration: none;
}
.not-found {
text-align: center;
padding: 60px 0;
}
本章小结
本章你学会了 React Router v6 的核心:路由表配置(createBrowserRouter)、Link 导航、Outlet 嵌套出口、动态路由 /post/:id、useParams 读取参数、useNavigate 编程式跳转。
现在博客有了真正的多页面体验——首页浏览列表,点击进入详情页阅读完整内容。
