前端路由到底是个啥?React Router v7 从零到一,大白话一次讲透

还在被路由配置绕晕?这篇文章用"说人话"的方式,带你彻底搞懂 HashRouter、懒加载、动态路由、嵌套路由......附带完整可运行代码 🔥

前言

最近在系统学 React 全家桶,搞到路由这卡了好久------不是概念多难,而是各种组件(Router、Routes、Route、Link、Outlet......)混在一起,不知道谁管谁的。踩了一堆坑之后,我用大白话把所有知识点串了一遍,分享出来。

你将会收获:

  • 前端路由到底是干嘛的,和以前的"后端路由"有啥区别
  • HashRouter 为什么用 # 号,背后的 hashchange 原理
  • 路由配置三件套:Router → Routes → Route,谁管谁一清二楚
  • 路由懒加载怎么配,lazy + Suspense 对首屏速度有什么帮助
  • 动态路由 :id 怎么用,useParams 怎么取参数
  • 404 页面兜底 + useNavigate 3 秒自动跳回首页
  • Navigate 重定向 ------ 旧地址自动跳新地址
  • 嵌套路由(二级路由)+ Outlet ------ 最难理解的部分用大白话讲透

技术栈: React 19 + react-router-dom v7 + Vite

一、前端路由到底是干嘛的?

没有前端路由的时候

以前网站全是后端渲染。你在地址栏输一个 URL,浏览器发请求给服务器,服务器返回一整个 HTML 页面。用户点一个链接 → 浏览器刷新 → 白一下 → 新页面出来。每点一次就完整刷新一次,慢,体验也不好。

bash 复制代码
┌──────────┐    请求 /about     ┌──────────┐
│  浏览器   │ ─────────────────→ │   后端    │
│          │ ←───────────────── │          │
│ (刷新)   │   返回完整HTML页面   │ 渲染页面  │
└──────────┘                    └──────────┘

前端路由做了什么

SPA(单页面应用)出来了------整个网站其实只有一个 HTML 文件 。不同"页面"的切换,本质上是不同的组件在同一个 <div> 里换着显示

那问题来了:用户点了导航链接,怎么知道该切换成哪个组件?这个"接管 URL 变化、根据 URL 决定显示哪个组件"的机制,这就是前端路由

对比维度 后端路由 前端路由
谁判断显示什么 服务端 浏览器里的 JS
切换时刷新页面? 是,会白一下 否,瞬间切换
速度 慢(等网络往返) 快(纯 JS 执行)
类比 每次换台要先关机重启 遥控器换台,秒切

HashRouter 为什么地址栏有个 #

前端路由有两种实现方式:History 模式和 Hash 模式。HashRouter 的原理特别直观:

shell 复制代码
#/           ← 首页
#/about      ← 关于页
#/user/123   ← 用户 123 页

# 后面的部分叫 hash (锚链接)。改变 hash 不会触发浏览器刷新页面 ------以前 <a href="#section2"> 做页内锚点跳转,跳完页面不刷新,就是这个原理。

前端路由正是利用了这个特性:只变 # 后面的内容,浏览器就不刷新,JS 自己监听变化来切组件

javascript 复制代码
// 原生 JS 监听 hash 变化
window.addEventListener('hashchange', () => {
  console.log('当前 hash:', window.location.hash)
  // 根据 hash 切换显示的组件
})

一句话记住:hash 变,页面不刷;hashchange 一监听到,JS 自己来切组件。

二、基本配置------路由三件套

先装依赖:

bash 复制代码
npm install react-router-dom

核心三件套:Router → Routes → Route

这三层是嵌套关系,缺一不可:

jsx 复制代码
import {
  HashRouter as Router,   // 声明用 hash 模式
  Routes,                 // 路由出口,扫描匹配
  Route,                  // 一条具体规则
} from 'react-router-dom'

const App = () => {
  return (
    <Router>                            {/* 第1层:注入路由能力 */}
      <Routes>                          {/* 第2层:找匹配的 Route */}
        <Route path="/" element={<Home />} />       {/* 第3层:规则 */}
        <Route path="/about" element={<About />} />
      </Routes>
    </Router>
  )
}

逐行拆解:

层级 组件 一句话作用
1 <Router> 最外层包裹,给整个应用注入路由能力(监听 hashchange)
2 <Routes> 遍历所有子 <Route>,找到 path 和当前 URL 匹配的那一条
3 <Route> 一条匹配规则:path="xxx" 定义 URL 模式,element={} 指定对应组件

Router 管能力,Routes 管匹配,Route 管映射。

不用 <a>,用 <Link> 跳转

jsx 复制代码
// Navigation.jsx
import { Link } from 'react-router-dom'

function Navigation() {
  return (
    <nav>
      <ul>
        <li><Link to="/">Home</Link></li>
        <li><Link to="/about">About</Link></li>
        <li><Link to="/user/123">小家</Link></li>
        <li><Link to="/products/123">产品列表</Link></li>
      </ul>
    </nav>
  )
}
<a href="/about"> <Link to="/about">
行为 浏览器向服务器发请求 JS 拦截,只改 URL hash
刷新页面? 是,会白一下 🔴 否,瞬间切换 🟢
适合 SPA? 否 🔴 是 🟢

三、路由懒加载------lazy + Suspense

没有懒加载的时候

jsx 复制代码
// ❌ 没有懒加载:所有页面组件一次性下载
import Home from './pages/Home'
import About from './pages/About'
import UserProfile from './pages/UserProfile'
// 用户只访问首页,但其他页面的代码也全下载了

不管用户访问哪个页面,所有页面组件的 JS 代码都会在打开网站时一次性全部下载。10 个页面就是 10 份代码,首屏特别慢。

加上懒加载

jsx 复制代码
import { lazy, Suspense } from 'react'

// ✅ 懒加载:用到哪个页面,才去下载哪个页面的代码
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
const UserProfile = lazy(() => import('./pages/UserProfile'))

const App = () => {
  return (
    <Router>
      <Suspense fallback={<div>Loading...</div>}>   {/* 下载中显示占位 */}
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </Suspense>
    </Router>
  )
}

lazy + Suspense 搭档原理:

markdown 复制代码
1. 用户打开 #/ → 只下载 Home 的代码          ← 首屏飞快
2. 用户点击 About → 触发懒加载,去下载 About
3. 下载期间 → Suspense 的 fallback 显示 "Loading..."
4. 下载完成 → 替换掉 fallback,显示 About 页面
概念 作用 类比
lazy(() => import(...)) 声明"用到时才加载" 点菜时才现做,不是提前全做完
<Suspense fallback={...}> 下载期间的占位内容 服务员说"稍等一下,马上来"

⚠️ 坑:只用 lazy 不写 Suspense 会直接报错,两者必须一起出现。

四、动态路由 + useParams

用户列表页,点"用户123"和"用户456"应该跳到不同页面。不可能给每个用户 ID 都写一条 Route。

用动态参数 :id

jsx 复制代码
// App.jsx 路由配置
<Route path="/user/:id" element={<UserProfile />} />

: 冒号开头表示这是个变量,可以匹配任意值:

shell 复制代码
#/user/123   →  :id = "123"
#/user/456   →  :id = "456"
#/user/abc   →  :id = "abc"

用 useParams 把参数取出来:

jsx 复制代码
// UserProfile/index.jsx
import { useParams } from 'react-router-dom'

function UserProfile() {
  let { id } = useParams()    // 从 URL 里取出 :id 的实际值
  console.log(id)             // "123"

  return <h1>User Profile: {id}</h1>
}

完整数据流:

ini 复制代码
┌──────────────┬──────────────────────────────────┐
│ 路由配置      │ <Route path="/user/:id" ...>     │
├──────────────┼──────────────────────────────────┤
│ 导航链接      │ <Link to="/user/123">            │
├──────────────┼──────────────────────────────────┤
│ 组件取参      │ let { id } = useParams()         │
├──────────────┼──────────────────────────────────┤
│ 渲染结果      │ "User Profile: 123"              │
└──────────────┴──────────────────────────────────┘

一句话记住:Link 传参(写在 URL 里),useParams 接参(从 URL 里读出来),: 冒号标记变量位。

五、404 Not Found + useNavigate

jsx 复制代码
// NotFound/index.jsx
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'

const NotFound = () => {
  let navigate = useNavigate()         // 拿到跳转函数

  useEffect(() => {
    setTimeout(() => {
      navigate('/')                    // 3 秒后跳回首页
    }, 3000)
  }, [])

  return <>404 Not Found</>
}

路由配置里用 path="*" 兜底:

jsx 复制代码
<Route path="*" element={<NotFound />} />   {/* * 通配符,上面都没匹配到就到这里 */}

path="*" 是通配符------上面所有 Route 都没匹配到的时候,它来兜底,给用户一个"页面不存在"的提示,而不是静默没反应。

Hook 作用
useNavigate() 返回一个跳转函数,代码里想跳哪就跳哪
navigate('/') 跳到首页,不刷新页面
useEffect(() => {...}, []) 组件挂载时执行一次(启动倒计时)

六、Navigate 重定向------旧地址自动跳新地址

jsx 复制代码
import { Navigate } from 'react-router-dom'

// 在 Routes 里面配一条重定向
<Route path="old-path" element={<Navigate to="/new-path" replace />} />
lua 复制代码
用户访问 #/old-path → Navigate 拦截 → 自动跳转 #/new-path

replace 属性表示"替换当前历史记录",这样用户按返回键不会回到旧地址(否则按返回又触发重定向,无限循环)。

七、嵌套路由(二级路由)+ Outlet------最难的部分

这是路由里最容易绕的一个概念。先看场景:

shell 复制代码
#/products          →  产品列表页
#/products/123      →  产品详情页(123 号产品)
#/products/new      →  新增产品页

products 下面有两个子页面,这种就叫嵌套路由(多级路由)。

父组件用 Outlet 给子路由留位置

jsx 复制代码
// Products/index.jsx
import { Outlet } from 'react-router-dom'

const Products = () => {
  return (
    <>
      <h1>产品列表</h1>
      <Outlet />   {/* ← 子路由的组件会渲染到这个地方 */}
    </>
  )
}

Outlet 就是子路由的"插座",没有它,子组件匹配到了也不知道该插到哪。

路由配置:子 Route 写进父 Route 里面

jsx 复制代码
// App.jsx
<Routes>
  <Route path="/products" element={<Products />}>         {/* 父路由,用 > 不是 /> */}
    <Route path=":productId" element={<ProductDetail />} /> {/* 子路由 */}
    <Route path="new" element={<NewProduct />} />           {/* 子路由 */}
  </Route>
</Routes>

关键语法:

jsx 复制代码
// ❌ 错误:父 Route 自闭合了 /> → 子路由进不去,报错
<Route path="/products" element={<Products />} />
  <Route path=":productId" element={<ProductDetail />} />

// ✅ 正确:用完整标签,子 Route 嵌套在里面
<Route path="/products" element={<Products />}>
  <Route path=":productId" element={<ProductDetail />} />
</Route>

子路由组件用 useParams 拿参数

jsx 复制代码
// Products/Detail/index.jsx
import { useParams } from 'react-router-dom'

const ProductDetail = () => {
  const { productId } = useParams()    // 拿到 :productId 的实际值
  return <h3>产品详情 {productId}</h3>
}

完整数据流

ini 复制代码
用户点 <Link to="/products/789"> 商品789 </Link>
       ↓
URL 变成 #/products/789
       ↓
Routes 匹配到 path="/products" + path=":productId" → productId = "789"
       ↓
Products 父组件渲染(标题"产品列表")
       ↓
Products 里的 <Outlet /> 渲染 ProductDetail 子组件
       ↓
ProductDetail 里 useParams() 拿到 { productId: "789" }
       ↓
最终显示:产品列表 / 产品详情 789

八、常见坑速查

原因 修复
页面完全空白 组件文件没有 export default 补上 export default Xxx
点击导航不切换 用了 <a> 而不是 <Link> 换成 <Link to="...">
懒加载报错 只有 lazy 没有 Suspense 包裹 外层加 <Suspense fallback={...}>
500 Internal Error 懒加载的路径对应的文件不存在 检查 import('./pages/Xxx') 路径
嵌套路由不显示 父 Route 用了自闭合 /> 改成 <Route ...>子路由</Route>

总结

核心概念速查表

概念 一句话
前端路由 JS 接管 URL 变化,不刷新页面,动态切换组件
HashRouter # 后的 hash 做路由,改变 hash 不刷新页面
Route path(URL 模式) → element(对应组件)的映射规则
Link SPA 专用跳转组件,替代 <a>,不刷新页面
lazy + Suspense 用到时才加载页面代码,首屏只下载当前页面
:id 动态路由 冒号标记变量位,同一组件适配不同 URL 参数
useParams Hook,从 URL 里取出 :id 的实际值
useNavigate Hook,用代码控制页面跳转
Navigate 重定向组件,旧 URL 自动跳新 URL
Outlet 嵌套路由的渲染出口,子组件插到这里

一句口诀

Router 管全局,Routes 管匹配,Route 管映射,Link 管跳转,Outlet 管嵌套,useParams 管取参。

核心代码骨架

jsx 复制代码
<Router>
  <Suspense fallback={<div>加载中...</div>}>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/user/:id" element={<UserProfile />} />
      <Route path="/products" element={<Products />}>
        <Route path=":productId" element={<ProductDetail />} />
      </Route>
      <Route path="*" element={<NotFound />} />
    </Routes>
  </Suspense>
</Router>

结尾

源码在我的 React 学习仓库 hjf-ai/fe/react/router/ 里,每个知识点都对应一个可运行的小 demo。路由这块彻底搞懂之后,后面学状态管理(zustand/pinia)就顺畅多了。

希望这篇文章对你有帮助!有问题欢迎在评论区交流 🔥

相关推荐
晨米酱2 小时前
Matt Pocock Skills v1.2:控制从主流程深入到每一步
面试·架构·agent
先吃饱再说2 小时前
从多页到单页:前端路由的演进与 React Router
前端·react.js·前端框架
北墨NoLimit2 小时前
TRAE Work实战:把办公Agent竞品调研从2-3天压到32分钟,有完整指令模板
前端·人工智能·数据可视化
颜进强2 小时前
Calude Code - 25 CodeGraph:让 AI 真正读懂你的代码库
前端·后端·ai编程
七牛开发者2 小时前
Agent 小知识|长任务不重来:Agent 状态保存的工程设计
前端·javascript·后端
weixin_431600443 小时前
做 Agent 会用到的 Node API(3):异步与流
前端·学习·ai·agent·ai编程
程序员黑豆3 小时前
鸿蒙应用开发之生命周期方法完全指南
前端·harmonyos
一点一木3 小时前
第 23 届 ChinaJoy 刚结束,我把 965 个展台做成了 3D 云展馆
前端·人工智能
wordbaby3 小时前
前端请求缓存写了,同一个接口为什么还是打了十几次?
前端·promise