现在位置: 首页 > React 教程 > 正文

路由跳转

本章你将学会用 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

两种路由模式: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>
)

在 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

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>
  )
}

不要用 <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>
  )
}

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>
}

Vue3 vs React 路由 API 对照

功能Vue3React
导航链接<RouterLink><Link>
路由出口<RouterView><Outlet>
动态路由/post/:id/post/:id
读取参数useRoute().paramsuseParams()
编程式跳转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

第二步:创建 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

实例

// 文件路径: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

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;
}

本章小结

本章你学会了 React Router v6 的核心:路由表配置(createBrowserRouter)、Link 导航、Outlet 嵌套出口、动态路由 /post/:id、useParams 读取参数、useNavigate 编程式跳转。

现在博客有了真正的多页面体验——首页浏览列表,点击进入详情页阅读完整内容。