前端路由技术详解:从传统多页到 React SPA 的完整进化之路

文章目录

    • [一、RESTful --- 一切皆资源](#一、RESTful — 一切皆资源)
    • 二、前端路由负责切换页面
      • [2.1 传统方式:后端路由 + 整页刷新](#2.1 传统方式:后端路由 + 整页刷新)
      • [2.2 SPA 的诞生:单页应用](#2.2 SPA 的诞生:单页应用)
      • [2.3 Hash 路由:改变 URL 但不刷新页面](#2.3 Hash 路由:改变 URL 但不刷新页面)
        • [URL 的结构](#URL 的结构)
        • [Hash 的原始用途:锚点定位](#Hash 的原始用途:锚点定位)
        • [Hash 路由的原理](#Hash 路由的原理)
        • [Hash 路由的工作流程图](#Hash 路由的工作流程图)
    • [三、React 集成前端路由](#三、React 集成前端路由)
      • [3.1 React 开发全家桶](#3.1 React 开发全家桶)
      • [3.2 使用 react-router-dom 搭建路由](#3.2 使用 react-router-dom 搭建路由)
    • 四、各种路由配置实战
      • [4.1 基本配置:Route + Routes + Link](#4.1 基本配置:Route + Routes + Link)
      • [4.2 路由懒加载:首页加载速度的"加速器"](#4.2 路由懒加载:首页加载速度的"加速器")
      • [4.3 动态路由:同一组件匹配不同参数](#4.3 动态路由:同一组件匹配不同参数)
      • [4.4 嵌套路由(二级路由)](#4.4 嵌套路由(二级路由))
      • [4.5 重定向](#4.5 重定向)
      • [4.6 404 Not Found 处理](#4.6 404 Not Found 处理)
    • 五、全文总结
    • 六、核心知识点复盘
    • [七、常见问题 / 避坑指南](#七、常见问题 / 避坑指南)

本文带你从"为什么需要前端路由"出发,一步步理解 Hash 路由的底层原理,再通过 React + react-router-dom 实战掌握路由配置、懒加载、动态路由和 404 处理。全程配合可运行代码,适合前端初学者系统学习,也适合有经验者查漏补缺。


一、RESTful --- 一切皆资源

在理解前端路由之前,有必要先聊一个重要的设计思想:RESTful

REST(Representational State Transfer)的核心原则是 "一切皆资源"。在 Web 开发中,每一个 URL 都代表一个资源:

复制代码
GET    /users          → 获取用户列表
GET    /users/123      → 获取 ID 为 123 的用户
POST   /users          → 创建一个新用户
PUT    /users/123      → 更新 ID 为 123 的用户
DELETE /users/123      → 删除 ID 为 123 的用户

同一个 URL /users/123,通过不同的 HTTP 方法(GET、PUT、DELETE)表达对资源的不同操作。这就是"一切皆资源"的含义 ------ URL 定位资源,HTTP 方法定义行为。

这个思想为后来前端路由的诞生埋下了伏笔:URL 和页面内容是一一对应的,这个对应关系就是"路由"。


二、前端路由负责切换页面

2.1 传统方式:后端路由 + 整页刷新

在早期的 Web 开发中(jQuery 时代甚至更早),路由完全由后端控制。当时的流程是这样的:

复制代码
用户点击 <a href="/about">关于我们</a>
      ↓
浏览器向服务器发起 GET /about 请求
      ↓
服务器返回完整的 about.html 页面(包含 <html><head><body> 全部内容)
      ↓
浏览器丢弃当前页面,重新解析、渲染整个 HTML
      ↓
页面"白一下",然后显示新内容

下面是一个传统多页应用的例子:

html 复制代码
<!-- demo/index.html --- 首页 -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>首页</title>
</head>
<body>
  <header>
    <nav>
      <ul>
        <li><a href="./index.html">首页</a></li>
        <!-- 点击这个链接会触发完整页面跳转 -->
        <li><a href="./about.html">关于我们</a></li>
      </ul>
    </nav>
  </header>
  <main>
    <h1>首页</h1>
  </main>
</body>
</html>
html 复制代码
<!-- demo/about.html --- 关于页面,另一个完全独立的 HTML 文件 -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>关于我们</title>
</head>
<body>
  <header>
    <nav>
      <ul>
        <li><a href="./index.html">首页</a></li>
        <li><a href="./about.html">关于我们</a></li>
      </ul>
    </nav>
  </header>
  <main>
    <h1>关于我们</h1>
  </main>
</body>
</html>

这种方式的核心问题非常明显:

问题 表现 原因
页面白屏 切换页面时短暂白一下 浏览器需要重新请求、解析、渲染整个 HTML
重复加载公共资源 导航栏、footer 等公共部分每次都重新渲染 每个 HTML 文件都包含完整的页面结构
用户体验差 有割裂感,不像"应用" 页面跳转是"销毁 → 重建",没有任何过渡

白话理解:传统方式就像每次换频道都要把电视关掉再打开 ------ 明明只是换个台,却要经历完整开关机流程,中间还会黑屏。这显然不够优雅。

2.2 SPA 的诞生:单页应用

进入移动互联网时代,用户已经习惯了 App 般流畅的体验。Web 页如果还有明显的"白屏切换",体验就太割裂了。

于是 SPA(Single Page Application,单页应用) 应运而生:

  • 核心思想 :整个应用只有一个 HTML 页面,所有"页面切换"都通过 JavaScript 动态替换页面内容来实现
  • 类比:就像一个舞台,背景、灯光都不变,只换台上的演员(内容区域)

那么问题来了 ------ 怎么在"不刷新页面"的前提下,让 URL 发生变化?

因为 URL 必须变 ------ 不同 URL 对应不同资源(RESTful 思想),用户要能收藏、分享链接。这需要一种"只改 URL 但不触发完整请求"的机制。

2.3 Hash 路由:改变 URL 但不刷新页面

答案藏在 URL 的一个特殊部分里 ------ hash(哈希/锚点)

URL 的结构

# 后面的部分就是 hash。浏览器对 hash 有一个关键特性

改变 hash 不会触发页面刷新,但浏览器的地址栏 URL 会更新,并且会在浏览器历史记录中插入一条新记录。

Hash 的原始用途:锚点定位

Hash 最初的设计目的其实是页面内定位(锚链接),用来在长页面中快速跳转:

html 复制代码
<!-- demo2/demo.html --- 锚链接的传统用法 -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>hash 锚点示例</title>
</head>
<body>
  <!-- 定义锚点位置 -->
  <a name="top"></a>
  <a href="#bottom">去到底部</a>
  <div style="height:200vh;background-color:yellow;"></div>
  <a href="#top">回到顶部</a>
  <div style="height:300vh;background-color:red;"></div>
  <a name="bottom"></a>

  <script>
    // 监听 hash 变化事件
    window.addEventListener('hashchange', function(event) {
      console.log('hash 改变了');
      console.log('新 URL:', event.newURL);
      console.log('旧 URL:', event.oldURL);
    });
  </script>
</body>
</html>

点击"去到底部"链接后,URL 从 demo.html 变成 demo.html#bottom ------ 页面没有刷新 ,但滚动到了 #bottom 锚点位置,同时触发了 hashchange 事件。

Hash 路由的原理

前端开发者巧妙地利用了 hash 的这个特性:用 hash 部分来表示前端路由路径

复制代码
https://www.example.com/#/home      → 首页
https://www.example.com/#/about     → 关于页
https://www.example.com/#/user/123  → 用户详情页

核心机制:

  1. 改变 URL :通过 location.hash<a href="#/xxx"> 修改 hash,页面不刷新
  2. 监听变化 :通过 hashchange 事件监听 hash 的变化
  3. 渲染内容:根据 hash 路径,动态替换页面对应区域的内容

下面是一个核心且完整的手写 HashRouter 实现:

js 复制代码
// demo2/index.html 的核心代码(简化并注释版)
// 手写一个简易 HashRouter,帮你彻底理解前端路由的本质

class HashRouter {
  constructor() {
    // 存储 hash 路径 → 回调函数的映射表
    // 例如:{ '/page1': fn1, '/page2': fn2 }
    this.routers = {};

    // 监听 hashchange 事件
    // 注意:事件回调中的 this 默认指向 window
    // 所以需要用 bind 把 this 绑定到 HashRouter 实例上
    window.addEventListener('hashchange', this.load.bind(this));
  }

  // 注册路由:将 hash 路径和对应的渲染函数关联起来
  register(hash, callback) {
    this.routers[hash] = callback;
  }

  // 加载页面:根据当前 hash 找到对应的 callback 并执行
  load() {
    // location.hash 获取的是带 # 的字符串,如 "#/page1"
    // 用 slice(1) 切掉第一个字符(#),得到 "/page1"
    let hash = location.hash.slice(1);

    let handler;
    if (!hash) {
      // 如果 hash 为空(刚打开页面时),匹配根路径 '/'
      handler = this.routers['/'];
    } else {
      handler = this.routers[hash];
    }

    // 如果找到了对应的回调函数,就执行它
    if (handler) {
      handler();
    }
  }
}

// ========== 使用示例 ==========
let router = new HashRouter();
let container = document.getElementById('container');

// 注册路由:hash 路径 → 渲染逻辑
router.register('/page1', function() {
  container.innerHTML = '<h1>页面一</h1>';
});
router.register('/page2', function() {
  container.innerHTML = '<h1>页面二</h1>';
});
router.register('/page3', function() {
  container.innerHTML = '<h1>页面三</h1>';
});

对应的 HTML 结构:

html 复制代码
<header>
  <nav>
    <ul>
      <!-- 注意 href 写的是 #/page1,不是 ./page1.html -->
      <li><a href="#/page1">页面一</a></li>
      <li><a href="#/page2">页面二</a></li>
      <li><a href="#/page3">页面三</a></li>
    </ul>
  </nav>
</header>
<!-- 页面内容的挂载点,SPA 中只需要动态修改这个 div -->
<div id="container"></div>

核心要点 :这个不到 30 行的 HashRouter 类就是所有前端路由库的底层原型。react-router-dom 的 HashRouter 本质上做了同样的事情,只是在此基础上增加了组件化、嵌套路由、路由守卫等高级功能。

Hash 路由的工作流程图
复制代码
用户点击 <a href="#/about">
        ↓
浏览器更新 URL 中的 hash 部分(页面不刷新!)
        ↓
触发 window 上的 hashchange 事件
        ↓
HashRouter.load() 被调用
        ↓
从 location.hash 中取出路径(如 "/about")
        ↓
在 routers 映射表中查找对应的回调函数
        ↓
执行回调,更新 container 的 innerHTML
        ↓
用户看到新"页面",但整个过程 HTML 从未重新加载

Hash 路由的三个关键认知:

  1. Hash 是 URL 的局部# 后面的内容不会发送给服务器,服务器永远只看到 # 前面的部分
  2. 改变 hash ≠ 刷新页面:浏览器把 hash 变化当作"页面内导航"而非"跨页面跳转"
  3. hashchange 是桥梁:连接"URL 变化"和"页面内容变化"的纽带

三、React 集成前端路由

3.1 React 开发全家桶

进入框架时代后,前端开发形成了标准化的技术栈。React 生态中通常包含:

角色 工具 作用
UI 框架 React 组件化开发、响应式数据、虚拟 DOM
前端路由 react-router-dom 给 SPA 添加路由能力
状态管理 Zustand / Redux 管理跨组件共享的数据
构建工具 Vite / Webpack 打包、热更新、代码分割

本文聚焦第二条 ------ react-router-dom,它是 React 官方推荐的路由库,底层基于 Hash 路由(或 History 路由)封装。

3.2 使用 react-router-dom 搭建路由

安装依赖:

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

核心组件及其职责:

组件 作用
HashRouter 最外层容器,启用 hash 路由模式(URL 带 #
Routes 路由规则集合,内部包含多个 Route
Route 单条路由规则,定义 path 和对应的 element
Link 替代 <a> 标签,实现 SPA 式导航(不刷新页面)
Suspense 懒加载时的"加载中"状态占位符

下面是一个完整的 React 路由入口配置:

js 复制代码
// src/main.jsx --- 应用入口
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.jsx';

// React 18 的创建方式:找到 #root 节点,渲染 App 组件
createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
);

四、各种路由配置实战

这是路由配置中最基础也最核心的部分。先看完整代码,再逐段解析:

js 复制代码
// src/App.jsx --- 路由的核心配置文件
import { lazy, Suspense } from 'react';
import {
  HashRouter as Router,  // 使用 Hash 路由模式,URL 以 # 开头
  Routes,                // 路由规则容器
  Route,                 // 单条路由规则
  Navigate               // 重定向组件
} from 'react-router-dom';
import Navigator from './components/Navigation';

// ========== 路由懒加载(详见 4.2 节) ==========
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
const NotFound = lazy(() => import('./pages/NotFound'));

const App = () => {
  return (
    <>
      {/* 1. HashRouter 是路由的根容器,所有路由组件必须放在它内部 */}
      <Router>
        {/* 2. Suspense 包裹懒加载组件,fallback 是加载中的占位 UI */}
        <Suspense fallback={<div>Loading...</div>}>
          {/* 3. Navigator 是导航栏组件,始终显示,不随路由变化 */}
          <Navigator />

          <div id="container">
            {/* 4. Routes 里配置所有路由规则,它决定了"哪个路径显示哪个组件" */}
            <Routes>
              {/* 基础路由:path="/" 对应首页组件 */}
              <Route path="/" element={<Home />} />

              {/* 基础路由:path="/about" 对应对应关于页组件 */}
              <Route path="/about" element={<About />} />

              {/* 动态路由::id 是路径参数,匹配 /user/123、/user/abc 等 */}
              <Route path="/user/:id" element={<UserProfile />} />

              {/* 重定向:访问旧路径自动跳转到新路径 */}
              <Route path="old-path" element={<Navigate replace to="/new-path" />} />

              {/* 
                404 兜底路由:
                path="*" 是通配符,匹配所有未被上面规则匹配的路径
                必须放在最后,否则会"吃掉"所有请求 
              */}
              <Route path="*" element={<NotFound />} />
            </Routes>
          </div>
        </Suspense>
      </Router>
    </>
  );
};

export default App;

导航组件使用 Link 而非 <a>

js 复制代码
// src/components/Navigation.jsx --- 导航组件
import { Link } from 'react-router-dom';

function Navigation() {
  return (
    <nav>
      <ul>
        {/* 
          用 <Link> 替代 <a>:
          - <a href="/about"> → 浏览器会发起完整请求,丢掉 SPA 状态
          - <Link to="/about"> → 只更新 URL hash,不刷新页面,保持应用状态
        */}
        <li><Link to="/">Home</Link></li>
        <li><Link to="/about">About</Link></li>
        <li><Link to="/user/123">小家</Link></li>
      </ul>
    </nav>
  );
}

export default Navigation;

页面组件示例:

js 复制代码
// src/pages/Home/index.jsx --- 首页组件
import './index.css';

export default function Home() {
  return <h1>Home</h1>;
}
js 复制代码
// src/pages/About/index.jsx --- 关于页组件
function About() {
  return (
    <div>
      <h1>About</h1>
    </div>
  );
}

// 这行 console.log 可以用来验证"懒加载组件何时被执行"
console.log('About 组件加载了');
export default About;

基本配置的关键理解:

概念 一句话说明
HashRouter 包在最外层,告诉 React "我们要用 hash 模式做路由"
Routes 路由规则集合,它内部有且只有一个 Route 会被渲染(匹配当前路径的那个)
Route 一条规则:path 是 URL 路径,element 是该路径要显示的组件
Link SPA 的 <a> 标签替代品,能更新 URL 但不刷新页面

4.2 路由懒加载:首页加载速度的"加速器"

为什么要懒加载?

在 SPA 中,如果不做任何优化,所有页面组件会在应用首次加载时一次性全部下载。这会导致:

  • 首页加载变慢(用户要等所有页面代码都下载完才能看到内容)
  • 浪费带宽(用户可能只访问首页,却被迫下载了 10 个不访问的页面)

懒加载的核心思路 :只加载当前访问的页面代码,其他页面的代码等到用户真正访问时才去下载

React 中的实现方式

React 提供了两个关键 API:

  • lazy() --- 接收一个返回 import() 的函数,返回一个"懒组件"
  • Suspense --- 包裹懒组件,在懒组件下载期间显示 fallback 内容
js 复制代码
import { lazy, Suspense } from 'react';

// ❌ 传统方式:同步 import,所有组件都会被打包进主 bundle
// import Home from './pages/Home';
// import About from './pages/About';

// ✅ 懒加载方式:动态 import(),每个页面独立打包,按需下载
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
const NotFound = lazy(() => import('./pages/NotFound'));

// 使用时必须用 Suspense 包裹
<Suspense fallback={<div>Loading...</div>}>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
    {/* ... */}
  </Routes>
</Suspense>

懒加载的完整工作流程

复制代码
用户首次访问 / (首页)
        ↓
打包后的 main.js 加载 → 包含 App、Navigator 等公共代码
        ↓
React 看到 <Route path="/" element={<Home />}>
        ↓
Home 组件是 lazy 的,触发 import('./pages/Home')
        ↓
浏览器下载 Home 组件的独立 chunk 文件
        ↓
下载完成,Home 组件渲染到页面上

----------- 此时 About、UserProfile 等页面还未下载 -----------

用户点击 <Link to="/about">
        ↓
React 看到需要 <About /> 组件
        ↓
触发 import('./pages/About')
        ↓
浏览器下载 About 的 chunk 文件
        ↓
下载完成,About 组件渲染

----------- 如果用户从未访问 /about,该 chunk 永远不会下载 -----------

Vite 和 Webpack 都原生支持 import() 语法,会对每个 import() 调用单独打包成一个 chunk 文件,实现真正的按需加载。

4.3 动态路由:同一组件匹配不同参数

很多时候,同一类页面只是数据不同,结构完全一样。比如:

  • /user/123 → 显示 ID 为 123 的用户信息
  • /user/456 → 显示 ID 为 456 的用户信息
  • /products/789 → 显示 ID 为 789 的商品详情

这时候如果为每个 ID 都写一个 Route,显然不现实。动态路由就是用来解决这个问题的:

js 复制代码
// App.jsx 中的配置
<Route path="/user/:id" element={<UserProfile />} />
//                  ↑
//            :id 是动态参数,冒号开头表示"这里是一个变量"

在组件内部,通过 useParams Hook 获取这个动态参数:

js 复制代码
// src/pages/UserProfile/index.jsx --- 动态路由页面
import { useParams } from 'react-router-dom';

function UserProfile() {
  // useParams() 返回一个对象,包含路径中所有的动态参数
  // 例如:URL 是 /user/123 → { id: "123" }
  let { id } = useParams();

  return (
    <>
      <h2>User Profile: {id}</h2>
    </>
  );
}

export default UserProfile;

关键认知useParams 是 React Router 提供的一个 Hook。它的"召之即来"特性意味着 ------ 你不需要手动从 URL 中解析参数,只要在 Route 的 path 里用 :参数名 声明,组件内就能通过 useParams() 直接拿到。

动态路由和普通路由的对比:

类型 路径写法 匹配示例 获取参数方式
普通路由 /about 只匹配 /about 无需参数
动态路由 /user/:id /user/123/user/abc useParams()
通配路由 * 匹配所有未被匹配的路径 无需参数(用于 404)

4.4 嵌套路由(二级路由)

当页面结构有层级关系时,嵌套路由非常有用。比如产品模块:

复制代码
/products           → 产品列表页(一级路由)
/products/123       → 产品详情页(二级路由,在列表页内的子区域展示)
/products/new       → 新增产品页(二级路由)

实现嵌套路由需要两个关键元素:

  • 父路由的 Route 不写 /* 后缀(react-router-dom v6+ 会自动处理子路由匹配)
  • 父组件内部放置 <Outlet /> 作为子路由的渲染出口
js 复制代码
// App.jsx 中的配置 --- 嵌套路由
<Routes>
  {/* 
    父路由路径是 "/products"
    子路由路径是相对路径 "new" 和 ":productId"
    实际完整路径是 "/products/new" 和 "/products/123" 
  */}
  <Route path="/products" element={<Products />}>
    <Route path=":productId" element={<ProductDetail />} />
    <Route path="new" element={<NewProduct />} />
  </Route>
</Routes>
js 复制代码
// src/pages/Products/index.jsx --- 父组件,包含 Outlet
import { Outlet, Link } from 'react-router-dom';

function Products() {
  return (
    <>
      <h1>产品列表</h1>
      <ul>
        <li><Link to="/products/123">商品详情</Link></li>
        <li><Link to="/products/new">新增商品</Link></li>
      </ul>

      {/* 
        Outlet 是关键!
        当访问 /products/123 时,<ProductDetail /> 渲染在这里
        当访问 /products/new 时,<NewProduct /> 渲染在这里
        当访问 /products 时,这里什么都不渲染(没有匹配子路由)
      */}
      <Outlet />
    </>
  );
}

export default Products;
js 复制代码
// src/pages/Products/Detail/index.jsx --- 子路由:产品详情
import { useParams } from 'react-router-dom';

const ProductDetail = () => {
  const params = useParams();
  return <h3>产品详情 {params.productId}</h3>;
};

export default ProductDetail;
js 复制代码
// src/pages/Products/New/index.jsx --- 子路由:新增产品
const NewProduct = () => {
  return <h3>New Product</h3>;
};

export default NewProduct;

一句话总结嵌套路由 :父 Route 套子 Route,父组件里放 Outlet。URL 路径 /products/123 中,/products 匹配父路由,/123 匹配子路由的 :productId 参数。

4.5 重定向

当某个旧路径需要跳转到新路径时,可以用 <Navigate> 组件:

js 复制代码
// replace 表示用新路径替换当前浏览器历史记录(用户按"后退"不会回到 old-path)
<Route path="old-path" element={<Navigate replace to="/new-path" />} />

4.6 404 Not Found 处理

404 是路由配置中的最后一道防线path="*" 表示匹配所有未被前面规则匹配的路径:

js 复制代码
// src/pages/NotFound/index.jsx --- 404 页面
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

function NotFound() {
  // useNavigate 返回一个跳转函数,等同于编程式导航
  let navigate = useNavigate();

  useEffect(() => {
    // 3 秒后自动跳回首页
    setTimeout(() => {
      navigate('/');  // 等价于 <Navigate to="/" />,但这是命令式的
    }, 3000);
  }, []);

  return (
    <div>
      <h1>404 Not Found</h1>
      <p>页面未找到,3 秒后自动返回首页...</p>
    </div>
  );
}

export default NotFound;

404 路由的放置位置非常关键 :必须放在 <Routes>最后一条 ,否则通配符 * 会抢先匹配所有路径,导致后面的路由永远不会被命中。

html 复制代码
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  {/* ✅ 404 放最后 --- 只有前面都没匹配到时才命中 */}
  <Route path="*" element={<NotFound />} />
</Routes>

五、全文总结

本文从前端路由的诞生背景和底层原理出发,系统性地梳理了:

  1. 为什么需要前端路由:从传统多页应用的"整页刷新、白屏切换"痛点,引出 SPA 的概念
  2. Hash 路由的工作原理 :利用 URL hash(# 之后的部分)改变时不刷新页面的特性,通过 hashchange 事件和路径-回调映射表实现前端路由
  3. React 中的路由实践 :用 react-router-dom 的 HashRouterRoutesRouteLink 搭建完整的路由系统
  4. 高级路由能力:懒加载(性能优化)、动态路由(参数化路径)、嵌套路由(层级页面结构)、404 处理(兜底策略)

六、核心知识点复盘

知识点 一句话核心
RESTful URL 定位资源,HTTP 方法定义行为,一切皆资源
SPA 只有一个 HTML 页面,通过 JS 动态切换内容,不整页刷新
Hash 路由 利用 URL 的 # 部分改变不刷新 + hashchange 事件实现前端路由
HashRouter react-router-dom 的 hash 路由容器,底层基于 hashchange
Routes / Route 路由规则配置:path 匹配路径,element 指定组件
Link vs <a> Link 更新 hash 不刷新页面;<a> 发起完整 HTTP 请求会刷新
lazy() + Suspense 懒加载:组件按需下载,提升首屏加载速度
:id 动态参数 路径中的变量,通过 useParams() 获取
Outlet 嵌套路由中,子路由组件的渲染出口
path="*" 通配符,匹配所有未命中路径,必须放在 Routes 最后(404 兜底)
useNavigate 命令式编程导航,适合在逻辑中控制跳转(如定时跳转)

七、常见问题 / 避坑指南

Q1:Hash 路由和 History 路由有什么区别?

对比维度 Hash 路由 History 路由
URL 形态 site.com/#/about site.com/about
实现原理 基于 hashchange 事件 基于 HTML5 History API(pushState/popState
服务器配置 无需任何配置 需要服务器配置 fallback(否则刷新 404)
适用场景 简单项目、无需后端配合 需要干净 URL、SEO 要求高

新手建议:先用 Hash 路由,理解原理后自然能过渡到 History 路由。

Q2:为什么 <Link><a> 不能混用?

<a href="/about"> 会让浏览器发起完整的 HTTP 请求,导致:

  • 整个 SPA 状态丢失(React 组件树被销毁重建)
  • 如果用的是 HashRouter,/about#/about 路径完全不匹配,可能直接 404

记住 :SPA 内的导航一律用 <Link>,只有跳转到外部链接时才用 <a>

Q3:动态路由参数获取不到?

jsx 复制代码
// ❌ 在 Route 之外使用 useParams
function MyComponent() {
  let { id } = useParams();  // 拿到的可能是 undefined
}

useParams() 只能在该组件被 <Route path="/user/:id"> 直接渲染时才能拿到参数。如果是一个嵌套很深的子组件,需要用 useParams 逐层传递,或者配合 Context 使用。

Q4:hashchange 事件中的 this 指向问题

手写 HashRouter 时,初学者容易踩这个坑:

js 复制代码
class HashRouter {
  constructor() {
    // ❌ 这样写,load 里的 this 会指向 window 而不是 HashRouter 实例
    window.addEventListener('hashchange', this.load);
    
    // ✅ 用 bind 绑定 this,或者用箭头函数
    window.addEventListener('hashchange', this.load.bind(this));
  }
}

原因是事件监听器中的回调函数,this 默认指向触发事件的对象(这里是 window),而非类的实例。这就是为什么 react-router-dom 等库会帮我们处理好这些细节 ------ 理解原理能帮你更快定位问题。


写在最后:前端路由是 SPA 的基石,理解它,你就打通了从"能做页面"到"能做应用"的关键一环。从手写 HashRouter 到在生产项目中使用 react-router-dom,本质上是同一个思路在不同抽象层次上的表达。希望本文能帮你建立起这个完整的认知链条。

相关推荐
Canace1 小时前
Harness Engineering 到底在做什么
前端·人工智能·ai编程
用户84298142418101 小时前
JS代码压缩实测:可减小体积、提高执行效率!
前端·javascript·后端
snow@li1 小时前
Vue Axios封装与SpringBoot Payload封装全景关联分析(前后端数据交互底层闭环)
前端·vue.js·spring boot
Csvn1 小时前
🐛 React StrictMode 下 useEffect 执行两次:不是 Bug,是特性
前端
用户2930750976691 小时前
React Hooks 与前端编程模型:从 DOM 操作到 Web Worker
react.js
做好一个小前端1 小时前
ECharts 折线图大数据量性能优化参考
前端·性能优化·echarts
渣波1 小时前
🧭 从 URL 到组件:深度剖析 React Router 原理与 SPA 路由架构艺术
前端
用户852495071841 小时前
一个 useRef,三种人生:从 DOM 引用到 Web Worker
前端
无人生还1 小时前
从 Vue3 到 React · 快速上手系列第 13 篇:工程化与综合实战(Vite + Todo 应用)
前端·vue.js·react.js