使用 History API

History API 使網站能夠與瀏覽器會話歷史記錄進行互動,即使用者在給定視窗中訪問過的頁面列表。當用戶訪問新頁面時,例如透過單擊連結,這些新頁面將被新增到會話歷史記錄中。使用者還可以使用瀏覽器的“後退”和“前進”按鈕在歷史記錄中前後移動。

History API 中定義的主要介面是 History 介面,它定義了兩個截然不同的方法集:

  1. 導航到會話歷史記錄中頁面的方法

  2. 修改會話歷史記錄的方法

在本指南中,我們將僅介紹第二組方法。

pushState() 方法向會話歷史記錄新增新條目,而 replaceState() 方法則更新當前頁面的會話歷史記錄條目。這兩個方法都接受一個 state 引數,該引數可以包含任何 可序列化物件。當瀏覽器導航到此歷史記錄條目時,瀏覽器會觸發一個 popstate 事件,該事件包含與該條目關聯的狀態物件。

這些 API 的主要目的是支援 單頁應用程式 等網站,這些應用程式使用 fetch() 等 JavaScript API 來用新內容更新頁面,而不是載入整個新頁面。

單頁應用程式和會話歷史記錄

傳統上,網站是作為頁面集合實現的。當用戶透過單擊連結導航到網站的不同部分時,瀏覽器每次都會載入一個全新的頁面。

雖然這對許多網站都很棒,但它可能存在一些缺點:

  • 每次都載入整個頁面可能效率低下,而實際上只需要更新頁面的一部分。
  • 跨頁面導航時維護應用程式狀態很困難。

出於這些原因,Web 應用程式的一個流行模式是 單頁應用程式 (SPA)。當用戶單擊連結時,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 result = await response.json();
      // Update the page with the new content
      displayContent(result);
    } catch (err) {
      console.error(err);
    }
  }
});

在此 click 處理程式中,如果連結包含資料屬性 "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()

我們可以如下在上面的 click 處理程式中新增一個歷史記錄條目:

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 result = await response.json();
      displayContent(result);
      // Add a new entry to the history.
      // This simulates loading a new page.
      history.pushState(result, "", creature);
    } catch (err) {
      console.error(err);
    }
  }
});

在這裡,我們使用三個引數呼叫 pushState()

  • result:這是我們剛剛獲取的內容。它將與歷史記錄條目一起儲存,並稍後作為傳遞給 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 中的連結,因此 click 處理程式會更新頁面並使用 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() 函式來更新頁面。

完整的 History API 示例

您可以在 https://github.com/mdn/dom-examples/tree/main/history-api 找到此完整示例,並可以在 https://mdn.github.io/dom-examples/history-api/ 上檢視即時演示。

另見