🔥还不会实现无刷新页面跳转?History API 与 Hash 路由全面解析🔥

155 阅读9分钟

引言

你有没有见过这样的场景:在浏览一个网站时,点击导航链接后,页面的内容迅速更新,而浏览器的地址栏也发生了变化,但整个页面并没有重新加载?

比如我们伟大的掘金:

image.png 这种流畅的用户体验正是前端路由技术带来的。传统的多页应用(MPA)在每个页面请求时都需要重新加载整个页面,这不仅增加了服务器的负担,也给用户带来了较差的体验。为了改善这一状况,单页应用(Single Page Application, SPA) 应运而生。(也就是在一个页面中实现多个页面的效果)

在SPA中,页面的主要内容通过JavaScript动态加载和更新,而不必重新加载整个页面,从而提供了更流畅、响应更快的用户体验。在这种背景下,前端路由技术成为了实现SPA的关键。前端路由允许开发者在不刷新页面的情况下更改URL并更新页面内容,使得用户感觉像是在不同的页面之间导航,但实际上只是页面的一部分发生了变化。这种技术不仅提高了用户体验,还简化了开发流程,使得Web应用更加动态和响应式。

在前端路由技术中,History APIhash 路由是最常用的两种方法。本文将深入探讨 History APIhash 路由的工作原理、使用场景、优缺点以及如何在实际项目中应用。

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 事件。 也就是这里👇👇👇:

image.png

我们浏览器有一个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 的主要用途:

  1. 无刷新改变 URL:修改 # 后面的内容不会导致浏览器重新加载页面。
  2. 前端路由导航:配合 hashchange 事件监听 URL 变化,实现 SPA(单页应用)路由。
  3. 锚点跳转(传统用法):#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.hashwindow.location.hash = "new-section"
浏览器前进/后退用户点击浏览器的前进/后退按钮(如果涉及 # 变化)

事件对象(event)属性

hashchange 触发时,事件对象包含:

属性说明兼容性
event.newURL变化后的完整 URLIE8+、现代浏览器
event.oldURL变化前的完整 URLIE8+、现代浏览器

#跳转

我们都知道可以利用#实现跳转,这个跳转可以这么用:

<a id="top"></a>

<a href="#top">回到顶部</a>

当我们点击一下就能回到<a id="top"></a>所在的位置, #可以绑定任何元素,但是仅建议绑定元素的id,因为它是唯一的,不容易出错。比如我们的掘金:

屏幕截图 2025-08-20 104405.png

掘金的文章是有跳转的,而且一般绑定在每个标题上。

我想用#跳转,但是恰好是个hash页面?

image.png

当页面中存在哈希(#)链接,同时需要实现 “回到顶部” 功能时,可能会遇到冲突,这个时候如果我们点击了最下方的回到顶部链接,会把#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事件,利用这个事件我们可以实现页面的热更新。