导航 API
导航 API 提供了启动、拦截和管理浏览器导航操作的能力。 它还可以检查应用程序的历史记录条目。 这是对以前 Web 平台功能(如 历史记录 API 和 window.location
)的继承,它解决了它们存在的缺陷,并且专门针对 单页应用程序 (SPA) 的需求。
概念和用法
在 SPA 中,页面模板在使用过程中往往保持不变,内容在用户访问不同的页面或功能时动态重写。 因此,浏览器中只加载了一个不同的页面,这打破了用户在查看历史记录中来回导航不同位置的预期体验。 这个问题可以通过 历史记录 API 在一定程度上得到解决,但它并非为 SPA 的需求而设计。 导航 API 旨在弥合这一差距。
可以通过 Window.navigation
属性访问 API,该属性返回对全局 Navigation
对象的引用。 每个 window
对象都有其自己的对应 navigation
实例。
处理导航
navigation
接口有几个关联的事件,最值得注意的是 navigate
事件。 当 任何类型的导航 启动时,它会被触发,这意味着你可以从一个中央位置控制所有页面导航,非常适合 SPA 框架中的路由功能。(历史记录 API 不是这样,有时很难弄清楚如何响应所有导航。)navigate
事件处理程序传递一个 NavigateEvent
对象,其中包含详细的信息,包括有关导航目标、类型、是否包含 POST
表单数据或下载请求等的详细信息。
NavigationEvent
对象还提供了两种方法
intercept()
接收一个回调处理程序函数作为参数,该函数返回一个 Promise。 它允许你控制启动导航时发生的事情。 例如,在 SPA 的情况下,它可以用来根据导航到的 URL 的路径将相关的新内容加载到 UI 中。scroll()
允许你手动启动浏览器的滚动行为(例如,到 URL 中的片段标识符),如果这对你的代码有意义,而不是等待浏览器自动处理它。
一旦导航启动,并且你的 intercept()
处理程序被调用,就会创建一个 NavigationTransition
对象实例(可以通过 Navigation.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>
中的导航,或跨源导航),提供所有先前历史记录条目的准确列表,只针对你的应用程序。 这使得遍历历史记录比使用旧的 历史记录 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 存在一些感知到的限制。
- 当前规范不会在页面首次加载时触发
navigate
事件。这对于使用服务器端渲染 (SSR) 的网站来说可能没问题 - 您的服务器可以返回正确的初始状态,这是将内容传递给用户的最快方式。但是,利用客户端代码来创建其页面的网站可能需要一个额外的函数来初始化页面。 - 导航 API 仅在单个帧内操作 - 顶层页面或单个特定的
<iframe>
。这有一些有趣的含义,在规范中进一步记录,但在实践中,将减少开发人员的困惑。之前的 历史记录 API 有一些令人困惑的边缘情况,例如对帧的支持,导航 API 在前端处理了这些情况。 - 您目前无法使用导航 API 以编程方式修改或重新排列历史记录列表。例如,导航用户到一个临时模态,要求他们提供一些信息,然后返回到之前的 URL,拥有临时状态可能是有用的。在这种情况下,您希望删除临时模态导航条目,这样用户就无法通过点击“前进”按钮并再次打开它来破坏应用程序流程。
接口
-
用于
navigate
事件的事件对象,当 任何类型的导航 被启动时触发。它提供了访问有关该导航的信息,最重要的是intercept()
,它允许您控制导航启动时发生的事情。 -
允许在一个中心位置控制当前
window
的所有导航操作,包括以编程方式启动导航、检查导航历史记录条目以及管理发生的导航。 -
表示最近的跨文档导航。它包含导航类型以及当前和目标文档历史记录条目。
-
用于
currententrychange
事件的事件对象,当Navigation.currentEntry
发生更改时触发。它提供了访问导航类型以及之前从其导航的历史记录条目的权限。 -
表示当前导航中正在导航到的目标。
-
表示单个导航历史记录条目。
-
表示正在进行的导航。
对其他接口的扩展
-
返回当前
window
的关联Navigation
对象。这是导航 API 的入口点。
示例
注意:查看 Domenic Denicola 的 导航 API 实时演示。
使用 intercept()
处理导航
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()
调用。
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);
},
});
}
});
遍历到特定历史记录条目
// 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;
更新状态
navigation.navigate(url, { state: newState });
或者
navigation.reload({ state: newState });
或者,如果状态独立于导航或重新加载
navigation.updateCurrentEntry({ state: newState });
规范
规范 |
---|
HTML 标准 # navigation-api |
浏览器兼容性
api.Navigation
BCD 表格仅在浏览器中加载
api.NavigationDestination
BCD 表格仅在浏览器中加载
api.NavigationHistoryEntry
BCD 表格仅在浏览器中加载
api.NavigationTransition
BCD 表格仅在浏览器中加载