JS-深度解密 History API:单页应用(SPA)实现无刷新跳转的底层逻辑

0 阅读3分钟

前言

在现代前端开发中,我们习惯了页面不刷新但 URL 却在变化的体验。这背后除了 Hash 模式,最核心的功臣就是 History 对象。它不仅能控制页面的前进后退,还能在不触发请求的情况下修改地址栏。

一、 History 对象:用户的“航迹云”

window.history 存储了用户在当前窗口中访问的所有记录。为了隐私保护,你无法看到具体的 URL 列表,但可以通过它自由穿梭。

1. 基础属性

  • length:历史堆栈中的条目总数(包括当前页)。
  • state:返回当前历史条目的状态对象副本。

2. 基础导航方法

  • back() :后退一页。

  • forward() :前进一页。

  • go(n)

    • n > 0:前进 n 步。
    • n < 0:后退 n 步。
    • n = 0 或不传:刷新当前页面

二、 HTML5 状态管理:无刷新跳转的核心

HTML5 为 history 引入了两个重量级方法:pushStatereplaceState。它们允许我们在不请求服务器的情况下,手动修改浏览器的地址栏。

1. pushState(state, title, url) —— 新增记录

  • 作用:在历史记录栈中添加一个新条目。

  • 参数

    • state:一个 JSON 对象,用于存储自定义信息。
    • title:目前大多数浏览器忽略,传 "" 即可。
    • url:新的 URL 地址,必须与当前页面同源

2. replaceState(state, title, url) —— 替换记录

  • 作用:修改当前的历史记录,而不是创建新的。
  • 影响:调用后 history.length 不会增加。

⚠️ 核心特点(避坑指南):

  • 不刷新:调用这两个方法后,地址栏变了,但浏览器不会检查 URL 是否存在,也不会重新加载页面。
  • 不触发 popstate:手动调用 pushStatereplaceState 不会触发 popstate 事件。

三、 popstate 事件:监听历史变动

虽然 pushState 不会触发 popstate,但浏览器的原生行为会触发它:

  • 点击“后退”或“前进”按钮。
  • 在 JS 中调用 back()forward()go()
window.addEventListener('popstate', (event) => {
    console.log("检测到路径变化,当前关联的状态数据:", event.state);
    // 这里通常是单页路由的核心:根据新的状态更新 UI 组件
});

四、 实战:为什么 Vue/React Router 需要它?

在 History 模式下,路由系统会拦截链接的默认点击行为,改为调用 history.pushState

  1. 用户点击链接 ➜ 拦截默认跳转 ➜ 执行 pushState('/new-path')
  2. 地址栏更新 ➜ 页面不刷新。
  3. 代码主动更新组件 ➜ 渲染新内容。
  4. 用户点击后退 ➜ 触发 popstate ➜ 路由监听到变化 ➜ 切换回旧组件。

五、 面试模拟题

Q1:history.pushStatelocation.href 有什么区别?

参考回答:

  • location.href 会导致浏览器立即向服务器发送请求,触发整页刷新。
  • history.pushState 仅修改浏览器地址栏并增加历史记录,不会触发网络请求,也不会刷新页面,是单页应用无刷新跳转的基础。

Q2:为什么 History 模式在刷新页面时会出现 404?如何解决?

参考回答: 因为 pushState 设置的 URL 是前端虚拟的。用户直接刷新时,浏览器会向服务器请求这个不存在的物理路径。 解决方法:需要后端(Nginx/Node)配合。将所有未命中的静态资源请求统一重定向(Fallback)到 index.html,让前端路由接管后续的解析。

Q3:replaceState 的典型应用场景是什么?

参考回答: 常用于重定向纠正 URL。例如:

  1. 用户登录后,从登录页跳转到个人中心,可以使用 replaceState 替换掉登录页。这样用户在个人中心点击“后退”时,不会再回到登录页,而是回到登录之前的页面。
  2. 在搜索页修改筛选条件时,实时更新 URL 却不希望产生过多的历史记录导致用户后退困难。