前言
在 React 单页应用(SPA)开发中,前端路由是整个应用的「骨架」。它让 URL 变化时无需刷新整页,只切换对应组件,既保证了交互流畅度,也让页面层级更清晰。
React Router v6 作为当前生态的主流路由方案,API 经过重构后更简洁,但也有不少容易踩坑的细节:比如 navigator 和 navigate 一字之差的区别、replace 到底要不要加、鉴权回跳为什么总是失效。
本文从底层原理、基础配置、动态路由、懒加载,到核心的鉴权守卫、登录回跳完整闭环,结合实战代码与高频踩坑,一次性把前端路由讲透。
一、先搞懂底层:SPA 与前端路由本质
1.1 为什么 SPA 需要前端路由
传统多页应用每次跳转都由后端返回新页面,体验割裂且性能差。而 SPA 的核心逻辑是:只有一个 HTML 入口,通过 JS 控制不同组件的渲染,URL 变化对应不同页面资源,全程不刷新整页。
前端路由就是负责管理「URL → 组件」映射关系的工具,核心解决两个问题:
- 如何修改 URL 且不触发页面刷新
- 如何监听 URL 变化,渲染对应组件
1.2 Hash 路由的实现原理
Hash 路由是最经典、兼容性最好的前端路由实现,利用 URL 中 # 后面的 hash 值变化不会触发页面刷新的特性,通过监听 hashchange 事件来切换组件。
plaintext
bash
http://localhost:3000/#/pay
http://localhost:3000/#/login
- 优点:兼容性好、无需后端配置
- 缺点:URL 带
#不美观,不利于 SEO
与之对应的是 History 模式(BrowserRouter),通过 history.pushState API 实现,URL 更美观,但需要后端配合做路径重定向。
1.3 别搞混:原生 BOM vs React Router API
很多初学者会把浏览器原生 API 和路由库 API 搞混,先理清边界:
原生浏览器对象(全局可用,无需导入)
表格
| 原生对象 | 核心作用 | 能否控制页面跳转 |
|---|---|---|
navigator |
获取浏览器、设备信息(userAgent、操作系统) | ❌ 完全不能跳转,和路由无关 |
window.location |
读写地址栏信息,可跳转、刷新页面 | ✅ 跳转触发整页刷新,无路由 state |
一句话记:
navigator管「浏览器是什么」,location管「当前地址是什么」。
React Router 提供的 Hook(需 import,仅路由组件内可用)
表格
| Hook | 返回值 | 核心作用 |
|---|---|---|
useNavigate() |
跳转函数 | 编程式控制路由跳转,无刷新 |
useLocation() |
路由信息对象 | 读取当前路径、search、hash、内存 state |
注意:
window.location没有路由state内存数据,state 只存在于 React Router 的useLocation返回对象中。
二、React Router v6 基础上手
2.1 安装与路由根容器
首先安装依赖:
bash
运行
npm install react-router-dom
在入口文件引入路由根容器,常用两种模式二选一:
HashRouter:Hash 模式,无需后端配置,新手推荐BrowserRouter:History 模式,URL 美观,需后端配合
jsx
javascript
// main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { HashRouter } from 'react-router-dom'
import App from './App.jsx'
ReactDOM.createRoot(document.getElementById('root')).render(
<HashRouter>
<App />
</HashRouter>
)
2.2 基础路由配置
v6 采用 Routes + Route 组合定义路径与组件的映射关系,自动匹配最优路径:
jsx
javascript
// App.jsx
import { Routes, Route } from 'react-router-dom'
import Home from './pages/Home'
import Login from './pages/Login'
import Pay from './pages/Pay'
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/pay" element={<Pay />} />
</Routes>
)
}
export default App
2.3 声明式跳转 Link
写在 JSX 里的跳转方式,相当于路由版的 a 标签:
jsx
javascript
import { Link } from 'react-router-dom'
<Link to="/pay">去支付页</Link>
三、常用路由场景全覆盖
3.1 动态路由:useParams 获取路径参数
当页面需要接收 URL 中的动态参数(如商品 ID、用户 ID)时,使用动态路由 + useParams Hook。
定义路由:
jsx
ini
<Route path="/goods/:id" element={<GoodsDetail />} />
组件内解构获取参数:
jsx
javascript
import { useParams } from 'react-router-dom'
const GoodsDetail = () => {
const { id } = useParams()
return <div>当前商品ID:{id}</div>
}
export default GoodsDetail
访问 /goods/1001 时,id 的值为字符串 '1001'。
3.2 路由懒加载:优化首屏体积
单页应用打包后体积较大,首屏加载慢。通过 React.lazy + Suspense 实现路由懒加载,访问对应路由时才加载组件代码。
jsx
javascript
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
// 懒加载引入组件
const Home = lazy(() => import('./pages/Home'))
const Pay = lazy(() => import('./pages/Pay'))
const Login = lazy(() => import('./pages/Login'))
function App() {
return (
<Suspense fallback={<div>页面加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/pay" element={<Pay />} />
</Routes>
</Suspense>
)
}
fallback 可放入加载动画,提升弱网下的用户体验。
3.3 404 兜底页面 + useNavigate 编程式跳转
使用通配符 * 匹配所有未定义路径,放在路由最底部作为兜底:
jsx
javascript
import NotFound from './pages/NotFound'
<Routes>
{/* 其他路由 */}
<Route path="*" element={<NotFound />} />
</Routes>
在 404 页面用 useNavigate 实现编程式跳转:
jsx
javascript
import { useNavigate } from 'react-router-dom'
const NotFound = () => {
const navigate = useNavigate()
return (
<div>
<h2>404 页面不存在</h2>
<button onClick={() => navigate('/')}>返回首页</button>
<button onClick={() => navigate(-1)}>返回上一页</button>
</div>
)
}
两种跳转方式对比:
<Link to="xxx">:声明式跳转,写在 JSX 里navigate(xxx):编程式跳转,写在事件回调、逻辑判断里
四、高阶实战:鉴权路由守卫(核心)
4.1 前置知识:HTTP 无状态与前端鉴权
HTTP 协议本身是无状态的,每次请求相互独立,服务器不知道用户是谁。但业务需要识别用户登录状态、控制页面权限,前端常见三种鉴权方案:
- Token 令牌 :登录成功后端返回 token,前端存储,后续请求放在请求头
Authorization中携带 - Cookie :浏览器自动携带,后端通过
Set-Cookie写入,有跨域限制 - localStorage:前端本地存储,纯前端鉴权、Demo 场景最常用
4.2 容器组件的核心:props.children
封装通用组件时,经常需要在双标签内部嵌套内容,通过 props.children 可以拿到组件声明时内部包裹的所有子节点。
jsx
javascript
// 容器组件
const Card = ({ children }) => {
return <div className="card">{children}</div>
}
// 使用
<Card>
<h3>标题</h3>
<p>内容</p>
</Card>
这是封装鉴权守卫、布局组件、弹窗等通用容器的基础特性。
4.3 手写鉴权守卫组件 ProtectRoute
核心思路:封装一个高阶组件,包裹需要登录才能访问的页面。进入组件时校验登录状态,未登录则跳转到登录页,同时记录当前页面路径,方便登录后回跳。
jsx
javascript
// components/ProtectRoute.jsx
import { Navigate, useLocation } from 'react-router-dom'
const ProtectRoute = ({ children }) => {
const location = useLocation()
// 从 localStorage 读取登录状态
const isLogin = localStorage.getItem('isLogin') === 'true'
if (!isLogin) {
// 未登录:跳转到登录页
// replace:覆盖当前历史记录,不新增登录页记录
// state.from:携带当前路径,登录成功后可回跳
return (
<Navigate
to="/login"
replace
state={{ from: location.pathname }}
/>
)
}
// 已登录:渲染被包裹的页面组件
return <>{children}</>
}
export default ProtectRoute
4.4 登录页实现:state 回跳 + replace 历史控制
登录页读取路由 state 中的来源路径,登录成功后跳转回去,并用 replace 覆盖登录页历史,避免用户回退又回到登录页。
jsx
javascript
// pages/Login.jsx
import { useNavigate, useLocation } from 'react-router-dom'
const Login = () => {
const navigate = useNavigate()
const location = useLocation()
// 读取来源路径,没有则默认跳首页
const from = location.state?.from || "/"
const handleSubmit = (e) => {
// 必须阻止表单原生提交刷新,否则会破坏路由历史
e.preventDefault()
const formData = new FormData(e.currentTarget)
const username = formData.get("username")
const password = formData.get("password")
if (!username || !password) {
alert("请输入用户名和密码")
return
}
if (username === 'admin' && password === '123456') {
// 写入登录状态
localStorage.setItem('isLogin', 'true')
// 跳回来源页面,replace 覆盖登录页历史
navigate(from, { replace: true })
} else {
alert("用户名或密码错误")
}
}
return (
<>
<h1>登录</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
name='username'
placeholder='请输入用户名'
required
/>
<input
type="password"
name='password'
placeholder='请输入密码'
required
/>
<button type='submit'>登录</button>
</form>
</>
)
}
export default Login
4.5 路由配置:用守卫包裹受保护页面
在路由配置中,用 ProtectRoute 包裹需要登录权限的页面即可:
jsx
xml
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/pay"
element={
<ProtectRoute>
<Pay />
</ProtectRoute>
}
/>
</Routes>
4.6 replace 到底有什么用?
肉眼看页面跳转效果完全一样,区别只在浏览器历史记录:
- 默认 push:新增一条历史记录,回退能回到上一页
- replace:覆盖当前历史记录,回退跳过本页
登录场景必须两处都加 replace:
- 守卫跳登录:把「目标页」替换成「登录页」,避免回退又回到未登录的目标页
- 登录成功跳回:把「登录页」替换成「目标页」,避免回退又回到登录页
五、高频踩坑避坑指南
5.1 表单提交必写 e.preventDefault ()
React 中使用 <form onSubmit> 时,浏览器默认会触发原生提交、刷新页面。不加 e.preventDefault() 会导致:
- 路由历史栈混乱,replace 失效
- 登录完点回退依然能看到登录页
记住:只要写表单 onSubmit,第一行永远写
e.preventDefault()。
5.2 两处 replace 缺一不可
很多人只在登录成功写 replace,忘记守卫组件的 <Navigate> 也要加 replace,导致回退依然能看到中间页。
5.3 state.from 的类型匹配
- 如果守卫传
location.pathname(字符串),登录页直接用location.state?.from - 如果守卫传完整
location对象,登录页要写location.state?.from?.pathname
类型不匹配会导致跳转路径为 undefined,页面空白。
5.4 别把 navigator 和 navigate 搞混
navigator:浏览器原生对象,查浏览器信息,不能跳转navigate:React Router 的跳转函数,用来控制路由
一字之差,功能完全无关。
5.5 useLocation /useNavigate 只能在路由组件内用
Hook 必须在被 HashRouter 包裹的组件内部调用,放在组件外部、if 语句内部都会报错。
六、总结
前端路由是 React SPA 开发的基础核心,从基础的页面切换到复杂的鉴权权限控制,本质都是围绕「URL 与组件的映射」展开。
本文覆盖的核心知识点:
- 底层原理:SPA 本质、Hash 路由原理、原生 BOM 与路由 API 的区别
- 基础能力:路由配置、动态参数、懒加载、404 兜底、两种跳转方式
- 高阶实战:鉴权守卫封装、登录回跳闭环、replace 历史控制
- 避坑指南:表单默认行为、两处 replace、类型匹配等高频坑