导航 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

另见