文章目录
-
- [一、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 → 用户详情页
核心机制:
- 改变 URL :通过
location.hash或<a href="#/xxx">修改 hash,页面不刷新 - 监听变化 :通过
hashchange事件监听 hash 的变化 - 渲染内容:根据 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 路由的三个关键认知:
- Hash 是 URL 的局部 :
#后面的内容不会发送给服务器,服务器永远只看到#前面的部分 - 改变 hash ≠ 刷新页面:浏览器把 hash 变化当作"页面内导航"而非"跨页面跳转"
- 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>
);
四、各种路由配置实战
4.1 基本配置:Route + Routes + Link
这是路由配置中最基础也最核心的部分。先看完整代码,再逐段解析:
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>
五、全文总结
本文从前端路由的诞生背景和底层原理出发,系统性地梳理了:
- 为什么需要前端路由:从传统多页应用的"整页刷新、白屏切换"痛点,引出 SPA 的概念
- Hash 路由的工作原理 :利用 URL hash(
#之后的部分)改变时不刷新页面的特性,通过hashchange事件和路径-回调映射表实现前端路由 - React 中的路由实践 :用 react-router-dom 的
HashRouter、Routes、Route、Link搭建完整的路由系统 - 高级路由能力:懒加载(性能优化)、动态路由(参数化路径)、嵌套路由(层级页面结构)、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,本质上是同一个思路在不同抽象层次上的表达。希望本文能帮你建立起这个完整的认知链条。