前端路由完全指南(上篇):从原理到 6 种路由模式

前言:为什么需要前端路由?

回想一下传统网站是怎么工作的:

arduino 复制代码
你点击"关于我们" → 浏览器向服务器发请求 → 服务器返回完整的 about.html → 页面白一下 → 显示新页面

每一次点击,都是一次完整的 HTTP 请求-响应周期。页面会整体刷新,出现短暂的白屏,体验就像是在翻阅一本很重的书------每次翻页都要用力。

而用了前端路由之后:

arduino 复制代码
你点击"关于我们" → JS 拦截点击 → 浏览器地址栏 URL 变化 → 只替换页面的一部分内容

没有白屏,没有整体刷新,就像在使用一个原生 App。

这就是 SPA(Single Page Application,单页应用)的核心体验。整个应用从头到尾只加载一个 HTML 文件,所有"页面切换"都是在这个文件里完成的。


一、SPA 的渲染链路

先搞清楚 SPA 是怎么跑起来的。以 React 项目为例,整个渲染链路只有三步:

less 复制代码
index.html                          ← ① 浏览器访问的唯一 HTML 页面
  └── <div id="root"></div>         ← 一个空的 div,React 的挂载点
  └── <script src="/src/main.jsx">  ← 加载 React 入口脚本
        │
        └── main.jsx                ← ② createRoot(document.getElementById('root'))
              └── .render(<App />)  ← ③ 将整个 React 组件树渲染进 #root

关键点在于:#root 是唯一的挂载点。之后所有"页面"的切换,其实都是在这个 div 内部替换不同的组件,浏览器根本没有刷新过。

这就是前端路由存在的意义------让 URL 变化时,不去找服务器要新页面,而是在本地匹配并渲染对应的组件。


二、前端路由的两种实现原理

前端路由有两种实现方式,分别对应两种 URL 格式。

2.1 HashRouter --- hash 模式

URL 长这样: http://localhost:5173/#/user/123

核心原理就一句话:监听 hashchange 事件 。URL 中 # 及其之后的部分叫 hash(锚点),它的变化不会触发页面刷新,也不会发送给服务器。

python 复制代码
用户点击链接 → URL 从 /#/home 变为 /#/user/123
                      ↓
浏览器触发 hashchange 事件
                      ↓
JS 读取 window.location.hash → "#/user/123"
                      ↓
去掉 # → "/user/123" → 匹配路由表 → 渲染 UserProfile 组件

服务器在这个过程中完全不知情------它只收到了 GET /,返回了 index.html,后面的事全是浏览器干的。

原理简化代码:

js 复制代码
window.addEventListener('hashchange', () => {
  const path = window.location.hash.slice(1) // "#/user/123" → "/user/123"
  renderComponent(path)
})

2.2 BrowserRouter --- history 模式

URL 长这样: http://localhost:5173/user/123

这是现在的主流方式,URL 看起来和普通网页一模一样。它依赖 HTML5 引入的三个 History API:

API 作用 React Router 中的对应
history.pushState(null, '', '/new') 改变 URL,向历史栈追加一条记录 <Link> 点击跳转
history.replaceState(null, '', '/new') 改变 URL,替换当前记录 <Navigate replace>
window.addEventListener('popstate', ...) 监听浏览器前进/后退按钮 路由的响应式更新

一次完整的点击跳转流程:

xml 复制代码
用户点击 <Link to="/about">
  │
  ├─ ① <Link> 拦截 <a> 标签的默认行为
  │    调用 history.pushState(null, '', '/about')
  │    浏览器地址栏变成 /about,但页面不刷新
  │
  ├─ ② BrowserRouter 检测到 URL 变化
  │    更新内部 location 状态,通知所有子组件
  │
  └─ ③ <Routes> 重新匹配
        /about → <About /> → 渲染 About 组件

原理简化代码:

js 复制代码
// 1. 拦截导航点击
function handleNavigation(path) {
  window.history.pushState(null, '', path) // 改 URL,不刷新
  renderComponent(path)                    // 渲染对应组件
}

// 2. 响应前进/后退
window.addEventListener('popstate', () => {
  renderComponent(window.location.pathname)
})

2.3 一张表看懂区别

维度 HashRouter BrowserRouter
URL 外观 /#/user/123 /user/123
底层原理 hashchange 事件 History API
# 后内容发往服务器? ❌ 不会 ✅ 会(是正常路径)
服务器需配置? 不需要 必须(见下篇)
SEO 好(配合 SSR)
适用场景 后台系统、内部工具 面向用户的 C 端站点

三、React Router 的核心组件

理解了原理,我们来看看 React Router 给我们提供了哪些组件来使用路由。

3.1 BrowserRouter --- 路由容器

jsx 复制代码
import { BrowserRouter as Router } from 'react-router-dom'

function App() {
  return (
    <Router>        {/* 包裹整个应用,提供路由上下文 */}
      <Navigation />
      <RouterConfig />
    </Router>
  )
}

BrowserRouter 是所有路由组件的"地基"。它在内部做了两件事:

  1. 监听 URL 变化(通过 History API)
  2. 向下传递路由上下文 (所有子组件才能使用 useNavigateuseParams 等 Hook)

如果你忘了包裹它,会直接报错:

scss 复制代码
❌ useLocation() may be used only in the context of a <Router> component.

3.2 Routes + Route --- 路由定义

jsx 复制代码
import { Routes, Route } from 'react-router-dom'

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  <Route path="*" element={<NotFound />} />
</Routes>
  • <Routes> :路由组的容器,内部 <Route> 的集合。它会遍历子路由,渲染第一个匹配的
  • <Route> :单条路由规则,path 定义匹配 URL,element 指定渲染哪个组件
jsx 复制代码
import { Link } from 'react-router-dom'

<Link to="/about">关于我们</Link>

<Link> 渲染后仍然是一个 <a> 标签,但它拦截了默认的点击行为 ,用 history.pushState 代替了浏览器的整页跳转。

组件 点击后
<a href="/about"> 整页刷新 ❌
<Link to="/about"> 局部更新 ✅

在 SPA 中,导航请用 <Link>,不要用 <a>

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

// 只要这个组件被渲染,就会立即重定向
<Navigate to="/login" />
<Navigate replace to="/new-path" />  {/* replace 模式 */}

它的特点是声明式------写在 JSX 里,渲染即跳转。适合用在路由配置中做 URL 迁移或鉴权拦截。

3.5 Outlet --- 嵌套路由的插槽

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

function Product() {
  return (
    <>
      <h1>产品列表</h1>        {/* 父组件自己的内容 */}
      <Outlet />               {/* 子路由的内容渲染在这里 */}
    </>
  )
}

<Outlet /> 就像一个"插槽"------父路由组件渲染时,匹配到的子路由组件会插入到这个位置。


四、6 种路由模式逐一拆解

理解了核心组件之后,我们来看看实际开发中会用到的所有路由模式。

模式 1:普通路由

最基础的用法------一个路径对应一个组件:

jsx 复制代码
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />

模式 2:动态路由

路径中用 :参数名 作为占位符,匹配任何值:

jsx 复制代码
<Route path="/user/:id" element={<UserProfile />} />
bash 复制代码
/user/12345  →  id = "12345"
/user/abc    →  id = "abc"
/user        →  不匹配(缺少参数)

在组件中通过 useParams() 获取参数值:

jsx 复制代码
function UserProfile() {
  const { id } = useParams()  // 访问 /user/123 → id = "123"
  return <h1>用户 {id}</h1>
}

URL 的结构回顾:

bash 复制代码
https://www.example.com/user/123?tab=posts#comment
  协议    域名          路径 动态参数 查询字符串   hash

模式 3:通配路由(404 页面)

* 匹配所有未被上面路由捕获的路径:

jsx 复制代码
<Route path="*" element={<NotFound />} />

必须放在所有路由的最后,否则会"吞噬"所有正常路由。

一个实用的 404 组件:显示提示后自动跳回首页:

jsx 复制代码
function NotFound() {
  const navigate = useNavigate()

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

  return <h1>404 Not Found</h1>
}

模式 4:嵌套路由

当多个页面共享同一个布局(侧边栏、标题等),用嵌套路由:

jsx 复制代码
// 路由定义
<Route path="/products" element={<Product />}>
  <Route path=":productId" element={<ProductDetail />} />
  <Route path="new" element={<NewProduct />} />
</Route>

// Product 父组件
function Product() {
  return (
    <>
      <h1>产品列表</h1>
      <nav>
        <Link to="/products/new">新增</Link>
        <Link to="/products/123">详情</Link>
      </nav>
      <Outlet />  {/* 子路由内容在这里渲染 */}
    </>
  )
}

访问 /products/123 时,渲染结果是:

html 复制代码
<h1>产品列表</h1>
<nav>...</nav>
<ProductDetail />   <!-- 通过 Outlet 插入 -->

子路由的 path 不需要写完整路径,只写相对于父路由的部分即可。

模式 5:鉴权路由(路由守卫)

有的页面需要登录后才能访问。我们可以创建一个"守卫组件"包裹受保护页面:

jsx 复制代码
// 路由定义
<Route path="/pay" element={
  <ProtectRoute>
    <Pay />           {/* 只有登录用户才能看到 */}
  </ProtectRoute>
} />

// ProtectRoute 组件
function ProtectRoute({ children }) {
  const isLogin = localStorage.getItem('isLogin') === 'true'

  if (!isLogin) {
    return <Navigate to="/login" />   // 未登录 → 跳转到登录页
  }

  return <>{children}</>              // 已登录 → 渲染子组件
}

这里的 children 是 React 的插槽机制------夹在 <ProtectRoute> 标签中间的内容会自动作为 children prop 传入。下面两种写法完全等价:

jsx 复制代码
<ProtectRoute>
  <Pay />
</ProtectRoute>

// 等同于
<ProtectRoute children={<Pay />} />

执行流程:

bash 复制代码
用户访问 /pay
  → ProtectRoute 检查 localStorage.isLogin
    ├─ true  → 渲染 <Pay />
    └─ false → <Navigate to="/login" />,URL 跳转到 /login

⚠️ 这是教学简化版(用 localStorage 模拟登录)。生产环境应使用 JWT 等真实认证方案。

模式 6:重定向路由

用于旧 URL 迁移或条件跳转:

jsx 复制代码
// 方式 1:声明式(路由配置中用)
<Route path="/old-path" element={<Navigate replace to="/new-path" />} />

// 方式 2:命令式(组件逻辑中用)
const navigate = useNavigate()
navigate('/new-path', { replace: true })
方式 类型 适用场景
<Navigate> 声明式(组件) 路由配置中固定重定向
useNavigate() 命令式(Hook) 在事件回调或逻辑中触发

五、这些模式如何串联在一起?

一个完整的路由配置大概是这样的:

jsx 复制代码
<BrowserRouter>
  <Navigation />       {/* 全局导航栏 */}

  <Suspense fallback={<LoadingFallback />}>
    <Routes>
      {/* 普通路由 */}
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />

      {/* 动态路由 */}
      <Route path="/user/:id" element={<UserProfile />} />

      {/* 嵌套路由 */}
      <Route path="/products" element={<Product />}>
        <Route path=":productId" element={<ProductDetail />} />
        <Route path="new" element={<NewProduct />} />
      </Route>

      {/* 鉴权路由 */}
      <Route path="/pay" element={
        <ProtectRoute><Pay /></ProtectRoute>
      } />

      {/* 重定向 */}
      <Route path="/old-path" element={<Navigate replace to="/new-path" />} />
      <Route path="/new-path" element={<NewPath />} />

      {/* 通配路由(必须放最后) */}
      <Route path="*" element={<NotFound />} />
    </Routes>
  </Suspense>
</BrowserRouter>

几个关键规则:

  1. <Routes> 从上到下匹配,命中即停
  2. 通配路由 path="*" 必须放最后
  3. Suspense 包裹 <Routes> 即可实现全局路由懒加载

总结

这篇我们搞清楚了三个核心问题:

  1. 前端路由解决什么问题? --- 实现 SPA,让页面切换不刷新
  2. 底层怎么实现? --- HashRouter 靠 hashchange,BrowserRouter 靠 History API
  3. 有哪些路由模式? --- 普通、动态、通配、嵌套、鉴权、重定向,共 6 种
相关推荐
像我这样帅的人丶你还1 小时前
🚀大文件上传的那些事
前端·javascript·架构
不好听6131 小时前
前端路由完全指南(中篇):React Router Hooks 完全指南
前端·react.js
hunterandroid1 小时前
[鸿蒙从零到一] ArkUI 声明式 UI 基础实战
前端
七牛开发者1 小时前
RAG 工程里的上下文剪枝:如何减少 68% 的上下文
前端·css·github
橘子星2 小时前
从零理解流式输出 —— 一个 Vue + DeepSeek 的前端实战
前端·javascript·人工智能
智驭未来掌门人2 小时前
如何让你的前端服务在服务器上nohub运行
前端
颜进强2 小时前
Claude Code - 18 从 Figma 到 React:一套让 AI 生成代码"不瞎编"的实战工作流
前端·后端·ai编程
李伟_Li慢慢2 小时前
12-mini版WebGLObjects
前端·three.js