http无状态?State来展示!从基础路由到鉴权守卫,吃透 SPA 前端路由核心

前言

在 React 单页应用(SPA)开发中,前端路由是整个应用的「骨架」。它让 URL 变化时无需刷新整页,只切换对应组件,既保证了交互流畅度,也让页面层级更清晰。

React Router v6 作为当前生态的主流路由方案,API 经过重构后更简洁,但也有不少容易踩坑的细节:比如 navigatornavigate 一字之差的区别、replace 到底要不要加、鉴权回跳为什么总是失效。

本文从底层原理、基础配置、动态路由、懒加载,到核心的鉴权守卫、登录回跳完整闭环,结合实战代码与高频踩坑,一次性把前端路由讲透。

一、先搞懂底层:SPA 与前端路由本质

1.1 为什么 SPA 需要前端路由

传统多页应用每次跳转都由后端返回新页面,体验割裂且性能差。而 SPA 的核心逻辑是:只有一个 HTML 入口,通过 JS 控制不同组件的渲染,URL 变化对应不同页面资源,全程不刷新整页

前端路由就是负责管理「URL → 组件」映射关系的工具,核心解决两个问题:

  1. 如何修改 URL 且不触发页面刷新
  2. 如何监听 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

写在 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 协议本身是无状态的,每次请求相互独立,服务器不知道用户是谁。但业务需要识别用户登录状态、控制页面权限,前端常见三种鉴权方案:

  1. Token 令牌 :登录成功后端返回 token,前端存储,后续请求放在请求头 Authorization 中携带
  2. Cookie :浏览器自动携带,后端通过 Set-Cookie 写入,有跨域限制
  3. 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:

  1. 守卫跳登录:把「目标页」替换成「登录页」,避免回退又回到未登录的目标页
  2. 登录成功跳回:把「登录页」替换成「目标页」,避免回退又回到登录页

五、高频踩坑避坑指南

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,页面空白。

  • navigator:浏览器原生对象,查浏览器信息,不能跳转
  • navigate:React Router 的跳转函数,用来控制路由

一字之差,功能完全无关。

5.5 useLocation /useNavigate 只能在路由组件内用

Hook 必须在被 HashRouter 包裹的组件内部调用,放在组件外部、if 语句内部都会报错。

六、总结

前端路由是 React SPA 开发的基础核心,从基础的页面切换到复杂的鉴权权限控制,本质都是围绕「URL 与组件的映射」展开。

本文覆盖的核心知识点:

  1. 底层原理:SPA 本质、Hash 路由原理、原生 BOM 与路由 API 的区别
  2. 基础能力:路由配置、动态参数、懒加载、404 兜底、两种跳转方式
  3. 高阶实战:鉴权守卫封装、登录回跳闭环、replace 历史控制
  4. 避坑指南:表单默认行为、两处 replace、类型匹配等高频坑
相关推荐
__zRainy__1 小时前
Node系列 · Node基础:全局变量与全局对象
开发语言·前端·javascript
码云骑士1 小时前
104-实战论文搜索引擎-ArXiv爬取-Milvus存储-RAG问答-Gradio前端
前端·python·搜索引擎·milvus
sunly_1 小时前
TypeScript总结:15、类型速查
前端·javascript·typescript
Brown.alexis1 小时前
es6知识点3-自备使用
前端·javascript·es6
2601_953988071 小时前
Ricon组态系统vs传统组态软件:为什么选择新一代Web组态平台
前端·后端·物联网·tcp/ip·数学建模·前端框架
天空之城--1 小时前
Claude Code 高效开发 Web 2D/3D 完全指南:心法、自定义 Skill 体系与社区技能包实战
前端·3d
IT_陈寒2 小时前
SpringBoot自动配置坑了我三天,原来漏了这个注解
前端·人工智能·后端
逝水无殇2 小时前
HTML 文本格式化详解:掌握 <strong>、<em>、<mark>、<del> 等常用标签
前端·javascript·html
特立独行的猫a2 小时前
DeepSeek Harness插件和工具的区别介绍及开发入门指南
前端·ai·agent·插件·deepseek·harness