導航 API

實驗性: 這是一項實驗性技術
在生產中使用此技術之前,請仔細檢查瀏覽器相容性表格

導航 API 提供了發起、攔截和管理瀏覽器導航操作的能力。它還可以檢查應用程式的歷史記錄條目。它是之前 Web 平臺功能(如 History APIwindow.location)的後續版本,解決了它們的缺點,並專門針對單頁應用(SPA)的需求。

概念與用法

在 SPA 中,頁面模板在使用過程中通常保持不變,內容會隨著使用者訪問不同的頁面或功能而動態重寫。因此,瀏覽器中只加載了一個不同的頁面,這打破了在檢視歷史記錄中不同位置之間來回導航的預期使用者體驗。這個問題可以透過 History API 在一定程度上解決,但它並非專為 SPA 的需求而設計。導航 API 旨在彌補這一差距。

透過 Window.navigation 屬性訪問此 API,該屬性返回對全域性 Navigation 物件的引用。每個 window 物件都有其自己的相應 navigation 例項。

處理導航

navigation 介面有幾個關聯事件,最值得注意的是 navigate 事件。當任何型別的導航被髮起時,此事件都會觸發,這意味著你可以從一箇中心位置控制所有頁面導航,這非常適合 SPA 框架中的路由功能。(這與 History API 不同,History API 有時很難找出如何響應所有導航。)navigate 事件處理程式會傳入一個 NavigateEvent 物件,其中包含詳細資訊,包括導航目的地、型別、是否包含 POST 表單資料或下載請求等。

NavigationEvent 物件還提供了兩個方法

  • intercept() 接受一個返回 Promise 的回撥處理函式作為引數。它允許你控制導航發起時發生的事情。例如,在 SPA 的情況下,它可以用於根據導航到的 URL 路徑將相關的新內容載入到 UI 中。
  • scroll() 允許你手動發起瀏覽器的滾動行為(例如,滾動到 URL 中的片段識別符號),如果你的程式碼需要這樣做,而不是等待瀏覽器自動處理。

一旦導航被髮起,並且你的 intercept() 處理程式被呼叫,就會建立一個 NavigationTransition 物件例項(可透過 Navigation.transition 訪問),該物件可用於跟蹤正在進行的導航過程。

注意: 在此上下文中,“transition”(過渡)指的是一個歷史記錄條目到另一個歷史記錄條目之間的過渡。它與 CSS 動畫無關。

注意: 你還可以呼叫 preventDefault() 來完全停止大多數導航型別的導航;遍歷導航的取消尚未實現。

intercept() 處理函式的 Promise 兌現時,Navigation 物件的 navigatesuccess 事件會觸發,允許你在成功導航完成後執行清理程式碼。如果它拒絕,意味著導航失敗,則會觸發 navigateerror,允許你優雅地處理失敗情況。NavigationTransition 物件上還有一個 finished 屬性,它與上述事件同時兌現或拒絕,為處理成功和失敗情況提供了另一種途徑。

注意: 在導航 API 可用之前,要實現類似的功能,你必須監聽所有連結的點選事件,執行 e.preventDefault(),執行相應的 History.pushState() 呼叫,然後根據新的 URL 設定頁面檢視。但這並不能處理所有導航,只能處理使用者發起的連結點選。

以程式設計方式更新和遍歷導航歷史記錄

當用戶瀏覽你的應用程式時,每次導航到新位置都會建立一個導航歷史記錄條目。每個歷史記錄條目都由一個獨立的 NavigationHistoryEntry 物件例項表示。這些物件包含多個屬性,例如條目的鍵、URL 和狀態資訊。你可以使用 Navigation.currentEntry 獲取使用者當前所在的條目,並使用 Navigation.entries() 獲取所有現有歷史記錄條目的陣列。每個 NavigationHistoryEntry 物件都有一個 dispose 事件,當該條目不再是瀏覽器歷史記錄的一部分時,該事件就會觸發。例如,如果使用者回退三次,然後向前導航到其他地方,那三個歷史記錄條目將被處置。

注意: 導航 API 只公開在當前瀏覽上下文中建立的、與當前頁面同源的歷史記錄條目(例如,不包括嵌入的 <iframe> 中的導航,或跨域導航),提供一個準確的、僅適用於你的應用程式的所有先前歷史記錄條目列表。這使得遍歷歷史記錄比使用舊的 History API 要穩健得多。

Navigation 物件包含了你更新和遍歷導航歷史所需的所有方法

導航到一個新的 URL,建立一個新的導航歷史記錄條目。

reload() 實驗性

重新載入當前的導航歷史記錄條目。

back() 實驗性

如果可能,導航到上一個導航歷史記錄條目。

forward() 實驗性

如果可能,導航到下一個導航歷史記錄條目。

traverseTo() 實驗性

導航到由其鍵值標識的特定導航歷史記錄條目,該鍵值透過相關條目的 NavigationHistoryEntry.key 屬性獲得。

以上每個方法都返回一個包含兩個 Promise 的物件 — { committed, finished }。這允許呼叫函式在以下情況發生之前等待進一步操作

  • committed 兌現,表示可見 URL 已更改並已建立新的 NavigationHistoryEntry
  • finished 兌現,表示你的 intercept() 處理程式返回的所有 Promise 都已兌現。這等同於 NavigationTransition.finished Promise 兌現,即在 navigatesuccess 事件觸發時,如前所述。
  • 上述任一 Promise 拒絕,意味著導航因某種原因失敗。

狀態

導航 API 允許你將狀態儲存在每個歷史記錄條目上。這是開發者定義的資訊 — 它可以是你喜歡的任何內容。例如,你可能希望儲存一個 visitCount 屬性來記錄檢視被訪問的次數,或者一個包含多個與 UI 狀態相關的屬性的物件,以便在使用者返回該檢視時可以恢復狀態。

要獲取 NavigationHistoryEntry 的狀態,你呼叫其 getState() 方法。它最初是 undefined,但當條目上設定了狀態資訊後,它將返回先前設定的狀態資訊。

設定狀態有點微妙。你不能直接檢索狀態值然後更新它 — 儲存在條目上的副本不會改變。相反,你在執行 navigate()reload() 時更新它 — 每個都可選地接受一個選項物件引數,其中包括一個 state 屬性,其中包含要設定在歷史記錄條目上的新狀態。當這些導航提交時,狀態更改將自動應用。

然而,在某些情況下,狀態更改將獨立於導航或重新載入——例如,當頁面包含可展開/可摺疊的 <details> 元素時。在這種情況下,你可能希望將展開/摺疊狀態儲存在歷史條目中,以便在使用者返回頁面或重新啟動瀏覽器時可以恢復它。此類情況透過 Navigation.updateCurrentEntry() 處理。currententrychange 事件將在當前條目更改完成後觸發。

侷限性

導航 API 存在一些公認的侷限性

  1. 當前規範不會在頁面首次載入時觸發 navigate 事件。這對於使用伺服器端渲染(SSR)的網站可能沒問題——你的伺服器可以返回正確的初始狀態,這是向用戶提供內容的最快方式。但利用客戶端程式碼建立頁面的網站可能需要額外的函式來初始化頁面。
  2. 導航 API 僅在單個幀內執行——頂級頁面,或單個特定的 <iframe>。這有一些有趣的含義,在規範中進一步有文件說明,但在實踐中,會減少開發人員的困惑。之前的 History API 有幾個令人困惑的邊緣情況,比如對幀的支援,而導航 API 預先處理了這些問題。
  3. 你目前不能使用導航 API 以程式設計方式修改或重新排列歷史記錄列表。擁有一個臨時狀態可能很有用,例如將使用者導航到一個臨時的模態框,向他們詢問一些資訊,然後返回到上一個 URL。在這種情況下,你可能希望刪除臨時的模態框導航條目,這樣使用者就不會透過點選前進按鈕再次開啟它來搞亂應用程式流程。

介面

任何型別的導航被髮起時,會觸發 navigate 事件。它提供了有關該導航的資訊,最值得注意的是 intercept(),它允許你控制導航發起時發生的事情。

允許在一箇中心位置控制當前 window 的所有導航操作,包括以程式設計方式發起導航、檢查導航歷史記錄條目以及管理正在發生的導航。

表示最近的跨文件導航。它包含導航型別以及當前和目標文件歷史記錄條目。

Navigation.currentEntry 更改時觸發的 currententrychange 事件的物件。它提供了訪問導航型別和從中導航的先前歷史條目的許可權。

表示當前導航中正在導航到的目的地。

表示單個導航歷史記錄條目。

表示正在進行的導航。

其他介面的擴充套件

Window.navigation 只讀 實驗性

返回當前 window 關聯的 Navigation 物件。這是導航 API 的入口點。

示例

使用 intercept() 處理導航

js
navigation.addEventListener("navigate", (event) => {
  // Exit early if this navigation shouldn't be intercepted,
  // e.g. if the navigation is cross-origin, or a download request
  if (shouldNotIntercept(event)) {
    return;
  }

  const url = new URL(event.destination.url);

  if (url.pathname.startsWith("/articles/")) {
    event.intercept({
      async handler() {
        // The URL has already changed, so show a placeholder while
        // fetching the new content, such as a spinner or loading page
        renderArticlePagePlaceholder();

        // Fetch the new content and display when ready
        const articleContent = await getArticleContent(url.pathname);
        renderArticlePage(articleContent);
      },
    });
  }
});

使用 scroll() 處理滾動

在這個攔截導航的例子中,handler() 函式首先獲取並渲染一些文章內容,然後獲取並渲染一些次要內容。一旦主要文章內容可用,立即將頁面滾動到該內容是有意義的,以便使用者可以與之互動,而不是等到次要內容也渲染完成。為了實現這一點,我們在兩者之間添加了一個 scroll() 呼叫。

js
navigation.addEventListener("navigate", (event) => {
  if (shouldNotIntercept(event)) {
    return;
  }
  const url = new URL(event.destination.url);

  if (url.pathname.startsWith("/articles/")) {
    event.intercept({
      async handler() {
        const articleContent = await getArticleContent(url.pathname);
        renderArticlePage(articleContent);

        event.scroll();

        const secondaryContent = await getSecondaryContent(url.pathname);
        addSecondaryContent(secondaryContent);
      },
    });
  }
});

遍歷到特定的歷史記錄條目

js
// On JS startup, get the key of the first loaded page
// so the user can always go back there.
const { key } = navigation.currentEntry;
backToHomeButton.onclick = () => navigation.traverseTo(key);

// Navigate away, but the button will always work.
await navigation.navigate("/another_url").finished;

更新狀態

js
navigation.navigate(url, { state: newState });

或者

js
navigation.reload({ state: newState });

或者如果狀態獨立於導航或重新載入

js
navigation.updateCurrentEntry({ state: newState });

規範

規範
HTML
# navigation-api

瀏覽器相容性

api.Navigation

api.NavigationDestination

api.NavigationHistoryEntry

api.NavigationTransition

另見