还在分不清什么是单页应用(SPA)?
前端路由、
pushState
、hashchange
这些词把你绕晕了?今天,我就用小白能听懂的大白话,带你一口气搞定 SPA + 前端路由这套祖传组合拳!
一、先把话挑明:SPA 是啥?
SPA,全称 Single Page Application ,中文直译就是:单页应用。
单页?顾名思义:
整个网站其实就一个页面,一个 index.html
,剩下所有的"页面切换",全靠 JavaScript 在里面翻魔术,局部更新。
这可不是嘴上说说,真的是只有一个 HTML 文件:
- 没有
home.html
、about.html
、profile.html
这种文件存在。 - 有的只是一个大大的
<div id="root"></div>
。 - 页面级别内容都靠 React/Vue 这种框架,在这一个 div 里动态塞来塞去。
听起来离谱?可就是因为这样,SPA 才能:
- 首屏加载后不卡:只要首屏资源加载完,后面切换页面不需要重新向服务器请求 HTML。
- 体验顺滑:点菜单 URL 变了,但页面没刷新,也不会白屏。
- 写起来省心 :前端掌控一切页面逻辑,服务器只需要丢个
index.html
给你就行。
二、那多个页面是怎么"假装"出来的?
只靠一个 HTML,怎么假装有多个页面?
核心秘诀:前端路由 + 组件化
-
你会写很多页面级别的 React 组件,比如:
jsfunction Home() { return <h1>这是首页</h1> } function About() { return <h1>关于我们</h1> } function Dashboard() { return <h1>控制台</h1> }
这些就是你假装的"页面"。
-
然后你用
<Routes>
+<Route>
做个路由表:jsx<Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="/dashboard" element={<Dashboard />} /> </Routes>
React Router 就帮你看着当前 URL,把匹配到的页面组件挂到
<Routes>
这个"占位符"里。 -
当你点击菜单(用
<Link>
),或者在代码里手动跳转(useNavigate
),React Router 会用history.pushState
把地址栏 URL 改掉,但页面不刷新,只会把对应组件挂到占位符上。
看起来好像真的在多个页面之间跳来跳去,实际上只是:
- URL 变了
- 浏览器没刷
- React 把
<div id="root">
里的内容换了
三、URL 怎么能变而页面不刷新?魔法在这里!
先说大白话:
浏览器的地址栏一旦变了,按理说要向服务器重新请求资源,页面就会刷新。
但 SPA 偏偏就能做到:URL 变了,页面照样稳如老狗。
秘诀有俩:
#
(hash)- HTML5 的
history.pushState
1)祖传做法:hash + hashchange
还记得很老的网站里有"回到顶部"吗?点一下 URL 变成 http://xxx.com/#top
,页面只滚动,没有刷新。
这个 #
后面叫 hash(或 fragment),浏览器遇到 hash:
- 不会向服务器重新发请求
- 只是前端自己拿到
location.hash
去解析
聪明的前端就想了:
既然 hash 不会刷新页面,那干脆把路由信息塞 hash 里得了!
于是你会看到:
bash
http://example.com/#/
http://example.com/#/about
前端再配个监听器:
js
window.addEventListener('hashchange', () => {
console.log('哈,hash 变了!赶紧切换组件');
});
这样,点击 <a href="#/about">
:
- 浏览器地址栏 hash 变了
- 页面没刷新
hashchange
事件触发,前端拿到新的 hash,切换对应的组件
就这样,最早的 SPA 前端路由就有了雏形。
2)现代做法:pushState + popstate
后来 HTML5 出了 history.pushState
,前端又找到了新玩具。
用它可以:
js
history.pushState({}, '', '/about');
- 地址栏直接从
/
改成/about
。 - 没有
#
,URL 干净利索,看起来跟真实多页站点一模一样。 - 浏览器照样不刷新页面。
不过有个小坑:
-
pushState
只负责改地址栏,不会自动通知前端"喂,你该换组件了"。 -
所以框架自己要做监听,或者点后退/前进时靠
popstate
事件感知:jswindow.addEventListener('popstate', () => { console.log('用户点了返回,赶紧切换组件'); });
3)pushState
vs hash
hash | pushState | |
---|---|---|
URL 样式 | 有 #,丑 | 没 #,干净 |
会不会刷新 | 不会 | 不会 |
需要服务器配置 | 不需要 | 需要后端把所有路径都返回 index.html,否则直接访问 /about 会 404 |
SEO 友好 | 不好 | 好(可以和 SSR 搭配) |
所以:
- 小项目、纯静态托管:hash 路由省心省力。
- 正式网站、想搞 SEO:pushState 路由香。
四、React Router 里这套是怎么拼起来的?
来,快速回顾一遍你真正在写的东西:
jsx
import { BrowserRouter as Router, Routes, Route, Link, useNavigate } from 'react-router-dom';
function App() {
return (
<Router>
<nav>
<Link to="/">首页</Link> | <Link to="/about">关于</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Router>
);
}
function Home() {
const navigate = useNavigate();
return (
<>
<h1>这是首页</h1>
<button onClick={() => navigate('/about')}>点我去关于</button>
</>
);
}
function About() {
return <h1>这是关于页</h1>;
}
这里:
1️⃣ <Router>
--- 开启前端路由的开关
它是啥?
<Router>
(最常见的是 <BrowserRouter>
)是 React Router 的根组件。
可以理解成:
"从这里往下,我要开始用前端路由了,麻烦你把 URL、路由表、页面渲染全帮我管理起来!"
没 <Router>
,React Router 整个就不工作:
<Link>
不会拦截<a>
的点击行为<Routes>
也不会匹配 URLuseNavigate
也会失效(因为找不到上下文)
内部到底干了啥?
以 <BrowserRouter>
为例:
-
它内部会用到浏览器原生的 HTML5 History API:
history.pushState
:JS 改 URL 不刷新window.onpopstate
:监听浏览器的前进/后退按钮
-
它会把 当前 URL 和一个上下文对象(用 React Context)传给子组件用:
<Routes>
、<Link>
、useNavigate
全都从这个上下文里拿数据。
你需要记住一句话:
<BrowserRouter>
是你前端路由的大脑和中枢,没它啥都跑不起来。
2️⃣ <Link>
--- 代替 <a>
,劫持点击
它是啥?
在 React Router 里,<Link>
就是用来替代 <a>
的。
写法长得像 <a>
:
js
<Link to="/about">关于我们</Link>
但内部不是 <a>
原生跳转,而是:
- 阻止
<a>
的默认刷新行为(event.preventDefault()
) - 用
history.pushState
改 URL - 告诉路由系统:"嘿,URL 变了,麻烦重新匹配
<Route>
。"
为什么要这么做?
因为如果你写:
js
<a href="/about">关于我们</a>
浏览器会真跳到 /about
,这会向服务器重新发请求。
而 SPA 不需要新请求,只需要前端自己换内容就行,所以 <Link>
的核心目标就是:
点一下,只改 URL,不刷新页面!
内部原理:
<Link>
本质就是一个<a>
元素,只是点击时 JS 劫持了点击。- 调用的就是
history.pushState
(或者history.replaceState
,如果用<Link replace>
)。
3️⃣ <Routes>
& <Route>
--- 路由表 + 占位符
<Routes>
是啥?
它相当于:
"我在页面里留个位置,这里会根据当前 URL 渲染对应的页面组件。"
没有 <Routes>
,React Router 就不知道要把你 <Route>
定义的页面挂到哪。
<Route>
是啥?
<Route>
就是一条路由规则:
path
:匹配的 URLelement
:要挂哪个组件
例如:
jsx
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
- 如果当前 URL 是
/
,就渲染<Home />
。 - 如果是
/about
,就渲染<About />
。
内部原理:
<Routes>
会读取当前 URL(来自<Router>
提供的上下文)。- 遍历所有
<Route>
,找path
匹配的那一个。 - 把对应的
element
(页面组件)渲染出来,挂到<Routes>
所在的位置。
所以你可以把 <Routes>
理解成:
"我在页面里占个位,谁匹配就谁上!"
4️⃣ useNavigate
--- JS 里手动跳转的万能钥匙
它是啥?
useNavigate
是一个 React Router 提供的 Hook,用来在 JS 逻辑里做路由跳转。
如果 <Link>
是 HTML 标签里的跳转,useNavigate
就是 JS 里条件跳转:
js
const navigate = useNavigate();
function handleLoginSuccess() {
// 登录成功后,跳转到首页
navigate('/');
}
内部干了啥?
navigate('/about')
就等于:
- 调用
history.pushState
改 URL - 触发路由系统重新匹配
<Route>
还能干嘛?
navigate
还能:
navigate(-1)
--- 返回上一页navigate('/about', { replace: true })
--- 用replaceState
,替换当前历史记录,不新增一条navigate('/about', { state: { foo: 'bar' } })
--- 可以带点状态
这四个是怎么串起来的?
我来给你梳理一下整个流程:
xml
1. 你写 <BrowserRouter> 把前端路由系统包起来,负责提供路由上下文。
2. 你用 <Link to="/about"> 拦截点击,内部用 pushState 改 URL。
3. URL 改了后,<Routes> 会检测当前 URL,找匹配的 <Route>,渲染对应组件。
4. 如果你需要 JS 里跳转(比如登录成功后自动跳转),就用 useNavigate 调用 pushState,一样走路由匹配。
✅ 核心记忆口诀
名字 | 干啥用 | 背后本质 |
---|---|---|
<Router> |
开启路由大脑 | 提供 URL、上下文、监听 popstate |
<Link> |
点一下改 URL,不刷新 | pushState |
<Routes> |
根据 URL 渲染页面 | 路由表匹配 |
useNavigate |
JS 里手动跳转 | pushState or replaceState |
这四个搞懂,你就能明白:
React Router = 前端路由的交通枢纽,URL 变了,页面只改局部,刷新?不存在的!
所以这套:
- 不刷新页面
- URL 在变
- 哪个页面显示由路由表决定
五、回到最初的问题:为什么这套能做到"看起来是多页,实际上是单页"?
总结核心点:
- 物理上只有一个 HTML。
- 逻辑上有很多页面级组件,挂到
<Routes>
里切换。 - URL 是状态来源,hash 或 pushState 改变它。
- 路由系统匹配 URL,决定挂哪个页面组件。
- 页面没刷新,只是 DOM 局部替换了内容。
- 对用户来说:没啥区别,就像真在多个页面跳转。
六、最后,这里可能有几个常见小白误区
<a>
和<Link>
有啥区别?
<a>
真跳转,浏览器会整页刷新;<Link>
拦截点击,用pushState
改 URL,不刷新。pushState
改 URL 为啥不自动换页面?
因为浏览器没义务管你页面内部渲染啥,路由框架自己得监听。- 为什么要服务器配置 fallback?
因为你访问example.com/about
,如果没有/about.html
,服务器就得返回index.html
,让前端自己解析/about
路由。
七、总结
单页应用(SPA)的本质就是:
一个页面假装成多个页面,靠前端路由 + URL 状态,做到"看起来多,实际只有一个"。
这套方案看着简单,背后就是:
- hash 路由的老把式
- HTML5 的新武器
history.pushState
- 框架里用
<Routes>
、<Route>
、<Link>
、useNavigate
串起来
吃透了这套,你就能随时在面试里从容说出:
"其实我们前端路由,就是用 pushState 或 hashchange,拦截点击,动态匹配路由表,局部更新组件,页面不刷新。"
面试官一听,这小子,行。
如果对你有用,别忘了点个赞!有疑问评论区见。
我是小阳,大白话聊前端,我们下次见!