能力 · 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"> 元素。
用单个 navigate 监听器渲染路由
Section titled “用单个 navigate 监听器渲染路由”一个监听器就能看到链接点击、表单提交、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() 走替换分支。
- View Transitions,多数 Navigation API 路由与
intercept()搭配的动画层 - Back/forward cache(bfcache),决定跨文档遍历是否恢复旧文档
- Speculation Rules API,预取
navigate处理函数即将请求的路由 - HTML Standard: the navigation API(html.spec.whatwg.org)
- HTML Standard: navigate() method(html.spec.whatwg.org)
- Modern client-side routing: the Navigation API(developer.chrome.com)
- WICG navigation-api explainer(github.com)
- Chromium bug 439994590:
javascript:URLs accepted by navigate()(crbug.com)
规范
| 规范 | 状态 |
|---|---|
| HTML Standard: the navigation API | WHATWG 现行标准 |
| HTML Standard: navigate() method | WHATWG 现行标准 |
| HTML Standard: NavigateEvent intercept() method | WHATWG 现行标准 |