跳转到内容

能力 · API

Navigation API

发布于 更新于

window.navigation 是 HTML 标准给「基于 history.pushState() 做路由」准备的替代品:文档发起的每一次导航都会触发同一个 navigate 事件,event.intercept() 把它转成由你的处理函数渲染的同文档切换。同一个对象还以 navigation.entries() 暴露会话历史,每个条目带稳定的 key,于是「后退」变成 navigation.traverseTo(key),不必再数 history.go() 的偏移量。

Chrome 102 在桌面与 Android 上发布了 Navigation,Edge 102 跟进(BCD api.Navigation);intercept() 方法在 Chrome 105 到来,取代了 Chrome 102 到 108 存在过的 transitionWhile()。Firefox 147 与 macOS、iOS 上的 Safari 26.2 同时发布了 Navigation、NavigateEvent 和 intercept()。Chrome 的 navigate() 仍接受 javascript: URL,与规范相悖(Chromium bug 439994590)。

navigation.navigate(url)
navigation.navigate(url, options)
navigation.reload()
navigation.reload(options)
navigation.traverseTo(key)
navigation.traverseTo(key, options)
navigation.back()
navigation.back(options)
navigation.forward()
navigation.forward(options)
navigation.updateCurrentEntry(options)
navigation.addEventListener("navigate", (event) => {
event.intercept(options);
});

navigate()、reload()、traverseTo()、back() 与 forward() 都返回 NavigationResult:一个含 committed 与 finished 两个 Promise 的普通对象,二者都以到达的 NavigationHistoryEntry 兑现。被 intercept() 接管的同文档导航中,committed 在 URL 与 navigation.currentEntry 更新后兑现,finished 在所有处理函数的 Promise 完结后兑现。跨文档导航,以及状态码 204、205 或带 Content-Disposition: attachment 头的响应,两个 Promise 都不会完结。intercept() 返回 undefined,只在 navigate 事件派发期间有效;updateCurrentEntry() 同步返回 undefined。

navigate() 接受一个按文档 base URL 解析的 url 字符串,外加可选的 NavigationNavigateOptions 字典。reload() 接受 NavigationReloadOptions,三个遍历方法接受 NavigationOptions,updateCurrentEntry() 接受 NavigationUpdateCurrentEntryOptions,其唯一成员 state 为必填。

成员 所属字典 类型 必填 说明
info NavigationOptions(所有方法) any 否 原样交给随之触发的 navigate 事件的 event.info,之后丢弃;用来传 UI 提示,例如滑动方向。
state NavigationNavigateOptions、NavigationReloadOptions、NavigationUpdateCurrentEntryOptions any,须可结构化序列化 否(updateCurrentEntry() 为必填) 存到新条目上,用 entry.getState() 读回。导航最终跨文档时被忽略。
history NavigationNavigateOptions "auto"、"push" 或 "replace" 否,默认 "auto" "auto" 推入新条目,除非这次导航必须是替换(与当前条目 URL 相同、初始的 about:blank 文档,或沙箱 iframe 这类只有一个条目的会话历史);这些情况下指定 "push" 会报错。

event.intercept() 接受 NavigationInterceptOptions 字典。多次调用会追加处理函数;后传的 focusReset 或 scroll 覆盖先传的,Chrome 会在控制台打印一条警告。

成员 类型 必填 说明
handler () => Promise<undefined> 否 URL 提交后运行。它的 Promise 决定 finished、navigation 上的 navigatesuccess 或 navigateerror 事件,以及 navigation.transition。
precommitHandler (controller) => Promise<undefined> 否 URL 变更前运行,可以调用 controller.redirect(url);只有 event.cancelable 为 true 时允许。
focusReset "after-transition" 或 "manual" 否 finished 完结后焦点是否移到第一个 autofocus 元素(没有则移到 <body>)。
scroll "after-transition" 或 "manual" 否 finished 完结后浏览器是否自动恢复(遍历)或重置(推入)滚动位置;选 "manual" 时在内容就绪后自行调用 event.scroll()。

NavigateEvent 本身带着路由决策所需的事实:canIntercept、destination(含 url、key、index、sameDocument 与 getState())、navigationType("push"、"replace"、"reload" 或 "traverse")、hashChange、downloadRequest、formData、userInitiated、signal 与 info。hasUAVisualTransition 在 Chrome 118 加入,sourceElement 在 Chrome 135 加入(BCD api.NavigateEvent)。

方法层面的失败以「early error result」形式呈现:committed 与 finished 以同一个 DOMException 拒绝。intercept() 与 scroll() 同步抛出。

异常 条件
SyntaxError navigate():url 按文档 base URL 解析失败。
NotSupportedError navigate():url 使用 javascript: 协议,或导航必须是替换却指定了 history: "push"。
DataCloneError navigate()、reload()、updateCurrentEntry():state 不可结构化序列化(函数、DOM 节点、WeakMap)。
InvalidStateError navigate() 与 reload():文档不是 fully active 或正在卸载。traverseTo():没有条目匹配该 key。back() 与 forward():currentEntry.index 已是 0 或最后一个。updateCurrentEntry():currentEntry 为 null。intercept():派发结束后调用、preventDefault() 之后调用,或在不可取消的事件上传了 precommitHandler。scroll():提交前调用或调用了第二次。
SecurityError intercept():canIntercept 为 false(目标跨源,或导航无法转为同文档),或事件不是可信事件。
AbortError committed 与 finished:导航被更新的导航取代、被用户停止加载,或被 preventDefault() 取消;event.signal 同一时刻触发 abort。

handler() 的 Promise 被拒绝不会产生上面任何名称:finished 以处理函数自己的错误拒绝,navigation 带着该错误触发 navigateerror。

三个示例共用一个前提:应用壳已在页面上,其中有一个供各路由填充的 <main id="view"> 元素。

一个监听器就能看到链接点击、表单提交、navigation.navigate() 调用以及前进后退遍历。路由不该接管的情况提前返回:跨源目标、仅片段变化、下载。handler() 运行时 URL 已经改变,所以先渲染占位,再把取回的内容换进去。

navigation.addEventListener("navigate", (event) => {
if (!event.canIntercept || event.hashChange || event.downloadRequest !== null) {
return;
}
const url = new URL(event.destination.url);
event.intercept({
async handler() {
const view = document.querySelector("#view");
view.setAttribute("aria-busy", "true");
const response = await fetch(`/fragments${url.pathname}`, { signal: event.signal });
view.innerHTML = await response.text();
view.removeAttribute("aria-busy");
},
});
});

把 event.signal 传给 fetch(),用户在请求完成前再次导航时请求会被取消;否则迟到的响应会覆盖更新的路由。

intercept() 缺失时回退到 History API

Section titled “intercept() 缺失时回退到 History API”

只检测 window.navigation 不足以判断实现可用:Chrome 102 到 104 暴露了 navigation 却没有 intercept(),为 intercept() 写的监听器在那里第一次点击就会抛 TypeError。检查原型上的方法,否则接上经典的 click 加 popstate 组合。

const hasNavigationApi =
"navigation" in window &&
typeof NavigateEvent !== "undefined" &&
typeof NavigateEvent.prototype.intercept === "function";
async function render(pathname) {
const response = await fetch(`/fragments${pathname}`);
document.querySelector("#view").innerHTML = await response.text();
}
if (hasNavigationApi) {
navigation.addEventListener("navigate", (event) => {
if (!event.canIntercept || event.hashChange || event.downloadRequest !== null) return;
const { pathname } = new URL(event.destination.url);
event.intercept({ handler: () => render(pathname) });
});
} else {
document.addEventListener("click", (event) => {
const link = event.target.closest("a[href]");
if (!link || link.origin !== location.origin || event.button !== 0 || event.metaKey || event.ctrlKey) return;
event.preventDefault();
history.pushState(null, "", link.href);
render(link.pathname);
});
window.addEventListener("popstate", () => render(location.pathname));
}

回退方案拿不到 Navigation API 白送的那部分:表单提交、location.assign() 调用,以及落到另一个文档上的遍历都绕过 click 监听器,直接整页重载。

用 traverseTo() 和存储的 state 回到已知条目

Section titled “用 traverseTo() 和存储的 state 回到已知条目”

navigation.entries() 返回每个同源条目,其 key 在重载后依然有效,向导式流程因此可以回到第一步而不必知道中间推入了几步。导航时把步骤存进 state,目标页就能恢复表单。

const startKey = navigation.currentEntry.key;
async function goToStep(step) {
const { finished } = navigation.navigate(`/checkout/${step}`, {
state: { step, startedAt: Date.now() },
});
await finished;
}
function restart() {
if (!navigation.entries().some((entry) => entry.key === startKey)) {
navigation.navigate("/checkout/1", { history: "replace" });
return;
}
navigation.traverseTo(startKey);
}
navigation.addEventListener("currententrychange", () => {
const state = navigation.currentEntry.getState();
if (state?.step) document.querySelector("#step").textContent = String(state.step);
});

用户若中途去过别的源再回来,startKey 已不在 entries() 里,traverseTo() 会以 InvalidStateError 拒绝;some() 检查让 restart() 走替换分支。

规范

规范状态
HTML Standard: the navigation APIWHATWG 现行标准
HTML Standard: navigate() methodWHATWG 现行标准
HTML Standard: NavigateEvent intercept() methodWHATWG 现行标准