你点的“刷新”是假刷新?前端路由的瞒天过海术
为什么单页应用切换页面时,浏览器没有真正刷新?地址栏变了,页面却没白一下?今天我们来拆穿前端路由的“魔术”——它根本没去服务器要新页面,而是自己偷偷换了内容。看完这篇,你也能实现一个自己的前端路由。
前言
你有没有注意过,现在很多网站(比如知乎、B站、Github)点开一个新页面,地址栏变了,但页面没有那种“白屏-加载-闪现”的过程,而是瞬间切换内容。这就像你走进一家餐厅,菜单上写着“换桌”,你以为换了个房间,结果服务员只是把你桌上的桌布换了。
这就是前端路由干的“好事”。它让页面看起来跳转了,实际上只是JS在背后偷偷换了DOM,地址栏的变化也是骗你的。今天我们就来揭开这个魔术的奥秘,顺便自己写一个简单的路由。
一、什么是前端路由?
传统网站,点击链接会向服务器请求一个新HTML,浏览器刷新整个页面。这叫后端路由。
单页应用(SPA)里,所有页面逻辑都在一个HTML里。切换“页面”时,不会请求新HTML,而是JS擦掉旧内容,画上新内容。同时,通过某种手段改变浏览器的地址栏URL,让用户感觉像换了个页面。这就是前端路由。
前端路由的实现依赖两个“戏法”:
- 改变URL但不刷新页面
- 监听URL变化并渲染对应组件
二、Hash模式:带#号的“假跳转”
早期前端路由用的是hash(也就是URL里#后面的部分)。改变#后的值,不会触发页面刷新,也不会向服务器发请求。浏览器自己会记录历史(前进后退可用)。
// 改变hash
window.location.hash = 'home';
// 监听hash变化
window.addEventListener('hashchange', () => {
const hash = window.location.hash.slice(1); // 去掉#
renderPage(hash);
});
比如https://example.com/#/home,你改成#/about,页面不会刷新,但hashchange事件会触发,你可以在回调里根据hash渲染不同内容。
优点:兼容性好,IE也能用。
缺点:URL有个丑陋的#;服务端无法捕获#后面的内容(因为#之后的部分不会发到服务器)。
三、History模式:看起来像真的
HTML5新增了pushState和replaceState,可以改变URL路径,同样不刷新页面。加上popstate事件监听,就能实现干净的路由(没有#)。
// 改变URL(添加一条历史记录)
history.pushState({ page: 'home' }, 'Home', '/home');
// 替换当前历史记录(不新增)
history.replaceState({ page: 'about' }, 'About', '/about');
// 监听前进后退
window.addEventListener('popstate', (event) => {
const state = event.state; // pushState时传的数据
renderPage(location.pathname);
});
优点:URL干净,像真实多页面。
缺点:需要服务端配合——因为刷新页面时,浏览器会按真实路径请求服务器,如果服务器没配置,会404。解决方案:所有路由都返回同一个HTML(即index.html)。
四、手写一个迷你前端路由
我们来实现一个最简单的Hash路由,包含三个“页面”:首页、关于、404。
<nav>
<a href="#/home">首页</a>
<a href="#/about">关于</a>
<a href="#/nothing">不存在</a>
</nav>
<div id="app">内容会变</div>
function renderPage(path) {
const app = document.getElementById('app');
if (path === '/home') {
app.innerHTML = '<h2>🏠 首页</h2><p>欢迎来到我的网站</p>';
} else if (path === '/about') {
app.innerHTML = '<h2>📖 关于</h2><p>这是一个前端路由演示</p>';
} else {
app.innerHTML = '<h2>❌ 404</h2><p>页面不存在</p>';
}
}
// 监听hash变化
window.addEventListener('hashchange', () => {
const hash = window.location.hash.slice(1); // 去掉#
renderPage(hash || '/home');
});
// 页面加载时执行一次
window.addEventListener('load', () => {
const hash = window.location.hash.slice(1);
renderPage(hash || '/home');
});
就这么几行,你已经实现了一个前端路由。当然,实际框架里的路由更复杂(嵌套路由、动态参数、路由守卫等),但核心原理就是监听URL变化 + 渲染对应组件。
五、前端路由与后端路由的区别
| 特性 | 后端路由 | 前端路由 |
|---|---|---|
| 请求方式 | 每次跳转都请求服务器 | 不请求服务器(JS切换内容) |
| 刷新页面 | 会重新下载HTML | 会刷新但需要服务端配合(history模式) |
| 首屏加载 | 只加载当前页面 | 通常要加载所有JS(可代码分割) |
| 用户体验 | 有白屏、闪烁 | 切换流畅 |
| SEO | 友好 | 较差(需SSR或预渲染) |
六、常见坑点与解决方案
1. History模式刷新404
配置Nginx将所有路由指向index.html:
location / {
try_files $uri $uri/ /index.html;
}
2. 路由跳转但页面不滚动
单页切换时,滚动条位置可能保留在上一个页面的位置。需要在路由变化后手动window.scrollTo(0, 0)。
3. 动态路由参数
比如/user/:id,你需要从路径中提取id。可以用正则或简单分割:
function matchRoute(path, routePath) {
const pathParts = path.split('/');
const routeParts = routePath.split('/');
if (pathParts.length !== routeParts.length) return null;
const params = {};
for (let i = 0; i < pathParts.length; i++) {
if (routeParts[i].startsWith(':')) {
params[routeParts[i].slice(1)] = pathParts[i];
} else if (routeParts[i] !== pathParts[i]) {
return null;
}
}
return params;
}
七、总结
- 前端路由让单页应用切换页面时不刷新,体验流畅。
-
Hash模式靠
#+hashchange,兼容性好,但URL丑。 -
History模式靠
pushState+popstate,URL干净,需服务端配合。 - 原理很简单:监听URL变化 → 根据路径渲染不同内容。
- 现代框架(React Router、Vue Router)都是在此基础上增强。
下次再看到地址栏变了但页面没白,你就可以自信地说:“哼,不过是在演我。”
如果你喜欢今天的“魔术揭秘”,点个赞让更多人看到。明天我们将聊聊Webpack的Loader和Plugin原理,从零理解构建工具的核心。我们明天见!