引言
你有没有见过这样的场景:在浏览一个网站时,点击导航链接后,页面的内容迅速更新,而浏览器的地址栏也发生了变化,但整个页面并没有重新加载?
比如我们伟大的掘金:
这种流畅的用户体验正是前端路由技术带来的。传统的多页应用(MPA)在每个页面请求时都需要重新加载整个页面,这不仅增加了服务器的负担,也给用户带来了较差的体验。为了改善这一状况,单页应用(Single Page Application, SPA) 应运而生。(也就是在一个页面中实现多个页面的效果)
在SPA中,页面的主要内容通过JavaScript动态加载和更新,而不必重新加载整个页面,从而提供了更流畅、响应更快的用户体验。在这种背景下,前端路由技术成为了实现SPA的关键。前端路由允许开发者在不刷新页面的情况下更改URL并更新页面内容,使得用户感觉像是在不同的页面之间导航,但实际上只是页面的一部分发生了变化。这种技术不仅提高了用户体验,还简化了开发流程,使得Web应用更加动态和响应式。
在前端路由技术中,History API 和 hash 路由是最常用的两种方法。本文将深入探讨 History API 和 hash 路由的工作原理、使用场景、优缺点以及如何在实际项目中应用。
History
什么是 History
History API 是浏览器提供的 JavaScript 接口,允许开发者在不刷新页面的情况下修改 URL 和管理浏览器历史记录。它是实现 SPA(单页应用)前端路由的核心技术之一,相比 hash 路由,它能生成更干净的 URL(如 /about 而不是 #/about),并提供更强大的历史记录控制能力。
例如现在主流框架中的 React和Vue 的路由就是利用History实现的
History怎么用?
history.pushState(state, title, url)
作用:向浏览器历史记录栈添加一个新条目,并更新 URL(不会触发页面刷新)。
参数:
state:一个 JavaScript 对象,用于存储与该历史记录条目相关的数据(可通过history.state访问)。title:目前大多数浏览器忽略此参数(通常传""或null)。url:新 URL(必须同源,即不能跨域)。
示例:
// 添加一条历史记录,在当前主页地址后加 /about
history.pushState({ page: "about" }, "", "/about");
此时浏览器地址栏会变成 /about,但页面不会刷新。
history.replaceState(state, title, url)
作用:替换当前历史记录条目(不会新增记录,也不会触发刷新)。
适用场景:比如登录后替换 /login 为 /dashboard,防止用户回退到登录页。
示例:
// 替换当前历史记录,URL 变为 /profile
history.replaceState({ page: "profile" }, "", "/profile");
history.go(n) / history.back() / history.forward()
history.go(n):跳转到历史记录的第n条(n可以是正数或负数)。history.back():后退(等同于history.go(-1))。history.forward():前进(等同于history.go(1))。
示例:
history.go(-2); // 后退两步
history.back(); // 后退一步
history.forward(); // 前进一步
popstate 事件
当用户点击浏览器的 前进/后退 按钮,或调用 history.go()、history.back() 等方法时,会触发 popstate 事件。
也就是这里👇👇👇:
我们浏览器有一个history栈,存储我们的历史记录,无论我们用history.go()、history.back(),实际上都是在操纵指针上下移动。
监听 popstate 事件:
window.addEventListener("popstate", (event) => {
console.log("当前状态数据:", event.state); // 获取 pushState/replaceState 存入的数据
console.log("当前 URL:", window.location.pathname);
});
注意:
pushState和replaceState不会触发popstate,只有 用户手动导航 或调用history.go()时才会触发。- 如果当前历史记录没有
state数据,event.state会是null。
History 实例
下面是我手写的一个History实例,来给大家看一下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>history SPA</title>
</head>
<body>
<h2>SPA路由模拟</h2>
<button onclick="navigate('/home')">首页</button>
<button onclick="navigate('/about')">关于我们</button>
<button onclick="navigate('/contact')">联系我们</button>
<button onclick="navigate('/login')">登录</button>
<button onclick="replace('/pay')">支付</button>
<a href="https://www.zhihu.com">知乎</a>
<div id="view">当前视图</div>
<script>
function render(path) {
document.getElementById('view').textContent = `当前视图:${path}`
// 用来更新页面
}
function replace(path) {
history.replaceState({ path }, '', path);
render(path);
}
function navigate(path) {
// history.pushState 入history 栈,不更新
history.pushState({ path }, '', path)
render(path) // 在修改完url后进行页面部分更新,达到一个页面能展示多个页面的效果
}
// 监听前进/后退事件
window.addEventListener('popstate', (event) => {
console.log('pop state fired:', event.state);
render(event.state?.path || location.pathname)
})
window.onload = () => {
const path = window.location.pathname;
if (path === '/') {
navigate('/home'); // 默认导航到首页
} else {
render(path);
}
};
</script>
</body>
</html>
Hash
Hash(哈希,即 URL 中的 #)是 前端路由 的另一种实现方式,它可以让网页在不刷新的情况下 改变 URL 并管理导航(前进/后退)。和 history.pushState() 不同,Hash 方式 兼容性更好,但 URL 会包含 # 符号。
Hash 的主要用途:
- 无刷新改变 URL:修改
#后面的内容不会导致浏览器重新加载页面。 - 前端路由导航:配合
hashchange事件监听 URL 变化,实现 SPA(单页应用)路由。 - 锚点跳转(传统用法):
#section1可直接跳转到页面的某个<div id="section1">位置。
Hash怎么用呢?
<!--直接在链接前加上 # 即可-->
<a href="#home">Home</a>
点击一下 url 就会加上 #home的后缀,表示是一个哈希。
以下我们通过一个例子,就能学会哈希的所有用法!
一个例子学会哈希的各种用法
接下来我们要做一个单页应用,实现不刷新页面的情况下改变url并且修改页面内容,
准备了几个要展示的小页面,分别为Home、About和Contact,我们用一个导航栏展示这三个链接,在连接的下面准备一个挂载点,以便展示不同页面的内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPA</title>
</head>
<body>
<a id="top"></a>
<h1>Navigation</h1>
<ul>
<li><a href="#home">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
<!-- #开头的叫锚点 -->
<div id="content-container" class="content">
Welcome,click on the links above to navigate
</div>
<div class="box" style="height: 200vh;"></div>
<a href="#top">回到顶部</a>
<script>
const content = document.getElementById('content-container')
// 监听hashchange事件
window.addEventListener('hashchange', function () {
switch (window.location.hash) {
case '#home':
content.innerHTML = '<h2>Home</h2> <p>Welcome to our homepage</p>';
break;
case '#about':
content.innerHTML = '<h2>About</h2> <p>Welcome to our about page</p>';
break;
case '#contact':
content.innerHTML = '<h2>Contact</h2> <p>Welcome to our contact page</p>';
break;
default:
content.innerHTML = 'Welcome,click on the links above to navigate';
}
})
</script>
</body>
</html>
Hash跳转
在这里我们要想实现跳转,就一定要用到哈希路由转换,于是定义了三个超链接。
让它们转换看起来更明显,证明它们可以实现局部更新而不刷新页面,我们在下面用了个content-container的挂载点,来承载它们不同的内容。
我们点一下超链接,就能实现url的改变了,但是我们的内容还没有变化,所以接下来要在JS里实现这一点:
什么时候content-container的内容会发生变化呢?
当然是我们点击这些哈希超链接的时候,点击他们的时候会触发一个事件叫hashChange,我们可以在全局Window里添加个监听器,监听此事件,一旦触发了这个事件我们就用JS修改content-container的内容值。
在这里修改内容的逻辑我们选用了switch-case,因为触发hashChange的值可以是很多种....接下来谁触发的hashChange,就改到什么样的值就好了。
window.location.hash
window.location.hash 是 JavaScript 中用于操作 URL 哈希部分的重要属性,它对应 URL 中 # 符号及其后面的内容。
hashChange 事件
hashchange 是浏览器提供的用于监听 URL 哈希部分(# 后面的内容)变化的事件,它在前端开发(尤其是单页应用 SPA)中有广泛应用。
hashChange会在以下场景触发:
| 触发场景 | 示例 |
|---|---|
点击带 # 的链接 | <a href="#section1">跳转</a> |
修改 location.hash | window.location.hash = "new-section" |
| 浏览器前进/后退 | 用户点击浏览器的前进/后退按钮(如果涉及 # 变化) |
事件对象(event)属性
当 hashchange 触发时,事件对象包含:
| 属性 | 说明 | 兼容性 |
|---|---|---|
event.newURL | 变化后的完整 URL | IE8+、现代浏览器 |
event.oldURL | 变化前的完整 URL | IE8+、现代浏览器 |
#跳转
我们都知道可以利用#实现跳转,这个跳转可以这么用:
<a id="top"></a>
<a href="#top">回到顶部</a>
当我们点击一下就能回到<a id="top"></a>所在的位置, #可以绑定任何元素,但是仅建议绑定元素的id,因为它是唯一的,不容易出错。比如我们的掘金:
掘金的文章是有跳转的,而且一般绑定在每个标题上。
我想用#跳转,但是恰好是个hash页面?
当页面中存在哈希(#)链接,同时需要实现 “回到顶部” 功能时,可能会遇到冲突,这个时候如果我们点击了最下方的回到顶部链接,会把#contact覆盖掉,仅剩下#top,因此在单页应用中,不太推荐这么使用跳转,而是用下面的button + function来解决:
function scrollToTop() {
window.scrollTo({
top: 0,
behavior: "smooth", // 平滑滚动
});
}
// 示例:按钮点击回到顶部
document.getElementById("back-to-top").addEventListener("click", scrollToTop);
为什么要使用history/hash,而不直接JS热修改?
有的同学学到这里可能有疑问了:“为了单页应用热修改,我直接用JS也行啊,为什么非得用history和hash?”
(热修改:不发生页面重载,直接更新页面)
拿上面例子来说,用JS进行热更新会遇到几个 问题:
- URL 不会变:用户无法通过地址栏直接访问
/about。 - 无法分享链接:别人复制你的页面 URL 时,永远看到的是首页。
- 浏览器前进/后退失效:用户点击后退按钮会直接离开你的网站。
所以我们就选用了history/hash。
history/hash不是用来“热更新”的,而是用来让无刷新页面跳转具备完整 Web 特性(URL 可访问、可导航、可分享)。
总结
这一期我们学习了前端路由的两个实现history/hash,它们都能实现让页面不重载就能更新,同时让这些页面具备完整的Web特性。
history.pushState()可以将url状态更新到history历史记录栈中,并修改url,但不会更新页面(需手动JS实现)。
popState是当我们用浏览器前进或后退时触发的事件,可以利用这个特性来实现快速热更新。
Hash 可以用来实现哈希超链接,其也会修改url并加入到history历史记录栈中,但也不会更新页面(也需要手动JS实现更新)
当我们修改window.location.hash或者点击hash超链接时,会触发hashChange事件,利用这个事件我们可以实现页面的热更新。