Navigation API · 一个事件接住所有导航
用 History API 搭 client router(见 客户端路由页)时,留下三处不便:pushState 改了栈却不发 popstate(得自己补渲染)、站内 <a> 点击要手动拦截、而且读不到完整的历史条目列表(只有一个
history.length 数字)。Navigation API 是为路由量身重做的一代:所有导航——链接点击、表单提交、navigate()、前进后退——统一汇成一个 navigate 事件,用 e.intercept({ handler }) 一处接管;还能直接读
navigation.entries()。
本页是真的在调用你浏览器里的 window.navigation。下面所有动作(按钮、那条普通链接、前进后退)都流经同一个 navigate 监听器——看右侧事件日志的 navigationType 怎么把它们分类,看左侧 navigation.entries() 实时增长。
浏览器支持。 Navigation API 已在 Chrome / Edge 102+、Firefox 147+、Safari 26.2+ 可用(核对于 2026-07)。下方的实时 demo 需要浏览器支持该 API;若你的浏览器偏旧,demo 会给出提示,但代码面板与对照表照常可读——升级或换 Chromium 打开即可看到真实行为。
注意那条普通链接。 它就是个 <a href>,没有给它绑任何 click 监听——点它,一样进了 navigate 事件、一样被 intercept 软处理、URL 一样不刷新整页。这正是相对 History API 最直接的简化:不必再到处
e.preventDefault() 拦 <a>,一个事件就把链接、表单、前进后退统一接住。
1 · 它到底解决了 History API 的什么
| 这件事 | History API | Navigation API |
|---|---|---|
| 感知一次导航 | pushState 不发 popstate,得在调用处自己补 |
统一的 navigate 事件,push / 前进后退都发 |
拦截 <a> 点击 |
手动 click + preventDefault + 判同源 |
同一个 navigate 事件就接住了 |
| 异步导航 / loading | 自己拼 pending、自己防竞态 | intercept({ handler }) 收一个 async 函数,内建 pending / 提交时机 |
| 读历史条目 | 只有 history.length 一个数字 |
navigation.entries() 拿到完整条目数组 + currentEntry |
| 能不能后退 | 没有可靠 API,只能猜 | navigation.canGoBack / canGoForward |
一句话:History API 是「操作历史栈的低层动作」,Navigation API 是「为客户端路由设计的高层模型」。
核心是一个 navigate 监听器接住一切,intercept 收一个 async handler:
// 一个监听器, 接住链接点击 / navigate() / 前进后退 —— 全都来这儿
navigation.addEventListener('navigate', (e) => {
if (!e.canIntercept || e.downloadRequest !== null) return; // 外链/下载: 放浏览器自己处理
e.intercept({ // ← 软处理: URL 更新, 但不整页刷新
async handler() {
const url = e.destination.url;
const data = await loadData(url); // 异步取数据, pending 状态浏览器替你管
render(url, data); // 数据到位才提交渲染
},
});
// e.navigationType: 'push' | 'replace' | 'traverse' | 'reload'
});
// 主动导航 / 前进后退, 也都会触发上面那个 navigate 事件
navigation.navigate('/products/42'); // push (默认)
navigation.navigate('/login', { history: 'replace' });
navigation.back(); navigation.forward();
// 完整历史 + 能力查询, History API 都给不了
navigation.entries(); // → 全部条目数组
navigation.currentEntry; // → 当前条目 (含 .index / .key / .url)
navigation.canGoBack; // → true / false
和客户端路由页对照着看。 那台 History API router 要写两个入口(popstate + 手动拦 <a>)、还要自己防异步竞态;这里收敛成一个 navigate 监听器,intercept 同时把 async
handler、pending、提交时机都管了。心智模型没变——还是那套「条目 + 指针」的 history 栈——变的是浏览器终于给了路由一套专门设计的原生接口。这也是为什么新框架的 router 正逐步改用它。
2 · 异步导航的时序 · committed / finished / signal
navigate() / reload() 都返回 { committed, finished } 两个 Promise:committed 在 URL 与新条目就绪时 resolve、finished 在 intercept 的 handler 跑完时 resolve;handler 收到的 e.signal 是一个
AbortSignal——下一次导航会自动 abort 上一次未完成的 handler,把它透传给 fetch 即可天然防竞态。点「快速连点」看前一次如何被中断(finished reject 出 AbortError)。
committed 与 finished 各管一段。 想在 URL 一变就更新地址栏 / 高亮当前导航项——等 committed;想在数据真正到位后再收尾(埋点、解除 loading)——等 finished。配套的全局事件 navigatesuccess /
navigateerror 是同一时机的文档级版本:任意一次软导航成功 / 失败都会派发,适合挂全局 loading 指示与错误提示。
上面主 demo 的几个控件演示了 History API 给不了的能力,可返回逐个操作:
-
navigate(url, { state }):每条历史条目挂一份 structured-clone 的state(看 entries 面板尾部 );后退 / 前进时currentEntry.getState()跟着条目回来,不进 URL、刷新不丢。 updateCurrentEntry({ state }):原地改当前条目 state——栈长度不变、不新增条目,只触发currententrychange。e.formData:那个<form method="post">没绑submit,提交照样进navigate事件、带着字段——表单和链接是同一套入口。
3 · intercept 的另外两个旋钮 · scroll 与 focusReset
intercept() 除了 handler,还收两个与无障碍相关的选项,默认都是 'auto'——浏览器替你把 SPA 软导航做得「像真导航」:push / replace 后滚到锚点或页首、traverse / reload 后恢复离开时的滚动位置;焦点则移到首个 [autofocus] 元素或
<body>。需要自己掌控时机(如等数据到位再滚)就设 'manual',再在合适时机调 e.scroll()。
| 选项 | 'auto' (默认) |
'manual' |
|---|---|---|
scroll |
push / replace 滚到锚点或页首;traverse / reload 恢复原滚动位置 | 浏览器不自动滚,由你在 handler 内择机调 e.scroll() |
focusReset |
导航后焦点移到首个 [autofocus],没有则 <body> |
焦点保持不动,由你自行管理 (如保留在搜索框) |
这两项是 Navigation API 相比 History API 的隐性收益:软导航默认就有正确的滚动 / 焦点行为,无障碍体验贴近真导航。
intercept 的完整形态——handler + scroll + focusReset + signal:
navigation.addEventListener('navigate', (e) => {
if (!e.canIntercept) return;
e.intercept({
scroll: 'manual', // 默认 'auto': 自动滚到锚点/页首、traverse 时恢复位置
focusReset: 'manual', // 默认 'auto': 焦点移到 [autofocus] 或 <body>
async handler() {
const res = await fetch(e.destination.url, { signal: e.signal }); // 下次导航自动 abort
render(await res.json());
e.scroll(); // manual 模式下, 数据到位后再自己触发滚动
},
});
});
4 · 事件家族 · 五个事件各管一段
| 事件 | 挂在哪 | 何时派发 |
|---|---|---|
navigate |
navigation | 任意导航发生前——唯一可 intercept / preventDefault 的入口 |
navigatesuccess |
navigation | intercept 的 handler 完成后 (软导航成功) |
navigateerror |
navigation | handler 抛错 / 被 abort——事件带 .error |
currententrychange |
navigation | 当前条目变化后 (含 updateCurrentEntry);导航已完成,不能再拦 |
dispose |
单个 NavigationHistoryEntry | 该条目被移出历史时 (如 push 截断了前进历史) |
前三个是「一次导航」的生命周期(开始 → 成功 / 失败);currententrychange 是「当前指针变了」的事后通知;dispose 则是某个条目的谢幕。
5 · 完整 API 速查
window.navigation 的方法 / 属性 / 条目,本页 demo 用到的都在这:
// 方法
navigation.navigate(url, { state, history: 'push' | 'replace', info });
navigation.reload({ state, info });
navigation.traverseTo(key); // 跳到指定 key 的条目, 比 go(n) 更稳
navigation.back(); navigation.forward();
navigation.updateCurrentEntry({ state }); // 原地改 state, 不导航
// 属性
navigation.currentEntry; // 当前 NavigationHistoryEntry
navigation.entries(); // 全部条目快照数组
navigation.canGoBack / canGoForward; // 能否前进 / 后退
navigation.transition; // 进行中的导航 (.finished); 空闲为 null
// NavigationHistoryEntry
entry.url / entry.key / entry.index; // key 跨 state 变化不变, index 可能随截断改变
entry.getState(); // 取该条目的 structured-clone state
entry.addEventListener('dispose', fn); // 条目被移出历史时
6 · 目前还差什么
Navigation API 收掉了 History API 的大半痛点,但仍有边界,以下限制需要了解:
- 首屏不触发。
navigate事件不会为初始页面加载派发——首屏渲染仍要在脚本里手动跑一次。 - 只管单个 frame。 一个
navigation对应一个 frame(顶层或某个 iframe),不跨 frame。 - 不能重排条目。 只能 push / replace / traverse,无法程序化插入、删除、重排历史条目。
- URL 先变、内容后到。 handler 跑完前 URL 已切换——异步加载期间应立即给出占位 / loading,而非空白。
7 · 相关页
- 本系列 · 客户端路由——先看 History API 版怎么搭 router、存在哪两处问题(
pushState不发popstate、手动拦<a>),再回来看本页怎么把它们一并消掉。 - 本系列 · 软导航——守卫的「取消 / 改道」、异步导航的 pending,在 Navigation API 里分别对应
e.preventDefault()与intercept的 async handler。 - MDN · Navigation API · developer.mozilla.org——
navigate事件、intercept、entries()、currententrychange的权威说明与浏览器兼容性。