使用 History API

History API 使网站能够与浏览器的会话历史记录进行交互:即用户在给定窗口中访问过的页面列表。当用户访问新页面时,例如通过点击链接,这些新页面就会被添加到会话历史记录中。用户也可以使用浏览器的“后退”和“前进”按钮在历史记录中来回移动。

History API 中定义的主要接口是 History 接口,它定义了两种截然不同的方法集

  1. 用于导航到会话历史记录中的页面的方法
  2. 用于修改会话历史记录的方法

在本指南中,我们将只关注第二组方法,因为它们的行为更复杂。

pushState() 方法向会话历史记录添加一个新的条目,而 replaceState() 方法更新当前页面的会话历史记录条目。这两种方法都接受一个 state 参数,该参数可以包含任何 可序列化对象。当浏览器导航到这个历史记录条目时,浏览器会触发一个 popstate 事件,该事件包含与该条目关联的状态对象。

这些 API 的主要目的是支持像 单页应用程序 这样的网站,这些网站使用 JavaScript API(如 fetch())来更新页面内容,而不是加载一个全新的页面。

单页应用程序和会话历史记录

传统上,网站是作为页面集合来实现的。当用户通过点击链接导航到网站的不同部分时,浏览器每次都会加载一个全新的页面。

虽然这对于许多网站来说都是很棒的,但它也有一些缺点

  • 每次只更新页面的一部分时,加载整个页面效率低下。
  • 在页面之间导航时,很难维护应用程序状态

出于这些原因,Web 应用程序中流行的一种模式是 单页应用程序 (SPA),其中网站仅包含一个页面,当用户点击链接时,页面会

  1. 阻止加载新页面的默认行为
  2. 获取 要显示的新内容
  3. 使用新内容更新页面

例如

js
document.addEventListener("click", async (event) => {
  const creature = event.target.getAttribute("data-creature");
  if (creature) {
    // Prevent a new page from loading
    event.preventDefault();
    try {
      // Fetch new content
      const response = await fetch(`creatures/${creature}.json`);
      const json = await response.json();
      // Update the page with the new content
      displayContent(json);
    } catch (err) {
      console.error(err);
    }
  }
});

在这个点击处理程序中,如果链接包含一个 "data-creature" 数据属性,那么我们使用该属性的值来获取一个 JSON 文件,其中包含页面的新内容。

JSON 文件可能看起来像这样

json
{
  "description": "Bald eagles are not actually bald.",
  "image": {
    "src": "images/eagle.jpg",
    "alt": "A bald eagle"
  },
  "name": "Eagle"
}

我们的 displayContent() 函数使用 JSON 更新页面

js
// Update the page with the new content
function displayContent(content) {
  document.title = `Creatures: ${content.name}`;

  const description = document.querySelector("#description");
  description.textContent = content.description;

  const photo = document.querySelector("#photo");
  photo.setAttribute("src", content.image.src);
  photo.setAttribute("alt", content.image.alt);
}

问题是它破坏了浏览器“后退”和“前进”按钮的预期行为。

从用户的角度来看,他们点击了一个链接,页面更新了,所以看起来像一个新页面。如果他们然后按下浏览器的“后退”按钮,他们希望返回到点击链接之前的状态。

但就浏览器而言,最后一个链接没有加载新页面,所以“后退”会将浏览器带到用户打开 SPA 之前加载的任何页面。

这本质上是 pushState()replaceState()popstate 事件要解决的问题。它们使我们能够合成历史记录条目,并能够在当前会话历史记录条目更改为这些条目之一时收到通知(例如,因为用户按下了“后退”或“前进”按钮)。

使用 pushState()

我们可以在上面的点击处理程序中添加一个历史记录条目,如下所示

js
document.addEventListener("click", async (event) => {
  const creature = event.target.getAttribute("data-creature");
  if (creature) {
    event.preventDefault();
    try {
      const response = await fetch(`creatures/${creature}.json`);
      const json = await response.json();
      displayContent(json);
      // Add a new entry to the history.
      // This simulates loading a new page.
      history.pushState(json, "", creature);
    } catch (err) {
      console.error(err);
    }
  }
});

在这里,我们使用三个参数调用 pushState()

  • json: 这是我们刚刚获取的内容。它将与历史记录条目一起存储,并在以后作为传递给 popstate 事件处理程序的参数的 state 属性包含在内。
  • "": 这是为了向后兼容传统网站,应始终为空字符串
  • creature: 这将用作条目的 URL。它将在浏览器的 URL 栏中显示,并将用作页面发出的任何 HTTP 请求的 Referer 标头的值。请注意,它必须与页面具有 相同来源

使用 popstate 事件

假设用户

  1. 点击了 SPA 中的一个链接,所以我们更新页面并使用 pushState() 添加了历史记录条目 A
  2. 点击了 SPA 中的另一个链接,所以我们更新页面并使用 pushState() 添加了历史记录条目 B
  3. 按下了“后退”按钮

现在新的当前历史记录条目是 A,所以浏览器触发了 popstate 事件,事件处理程序参数包含我们处理导航到 A 时传递给 pushState() 的 JSON。这意味着我们可以使用类似这样的事件处理程序恢复正确的内容

js
// Handle forward/back buttons
window.addEventListener("popstate", (event) => {
  // If a state has been provided, we have a "simulated" page
  // and we update the current page.
  if (event.state) {
    // Simulate the loading of the previous page
    displayContent(event.state);
  }
});

使用 replaceState()

我们还需要添加一个部分。当用户加载 SPA 时,浏览器会添加一个历史记录条目。由于这是一个实际的页面加载,所以该条目没有关联的状态。因此,假设用户

  1. 加载了 SPA:浏览器添加了一个历史记录条目
  2. 点击了 SPA 中的链接:点击处理程序更新页面并使用 pushState() 添加了一个历史记录条目
  3. 按下了“后退”按钮

现在我们想返回到 SPA 的初始状态,但由于这是同一个文档中的导航,页面不会重新加载,并且由于初始页面的历史记录条目没有状态,我们无法使用 popstate 来恢复它。

这里的解决方案是使用 replaceState() 来设置初始页面的状态对象。例如

js
// Create state on page load and replace the current history with it
const image = document.querySelector("#photo");
const initialState = {
  description: document.querySelector("#description").textContent,
  image: {
    src: image.getAttribute("src"),
    alt: image.getAttribute("alt"),
  },
  name: "Home",
};
history.replaceState(initialState, "", document.location.href);

在页面加载时,我们收集了在用户返回到 SPA 的起点时需要恢复的页面的所有部分。这与我们在处理其他导航时获取的 JSON 具有相同的结构。我们将这个 initialState 对象传递给 replaceState(),它实际上将状态对象添加到当前历史记录条目中。

当用户返回到我们的起点时,popstate 事件将包含这个初始状态,我们可以使用我们的 displayContent() 函数来更新页面。

完整示例

您可以在 https://github.com/mdn/dom-examples/tree/main/history-api 中找到这个完整的示例,并在 https://mdn.github.io/dom-examples/history-api/ 中查看演示。

另请参阅