Navigation API · 一个事件接住所有导航
用 History API 搭 client router(见客户端路由)时留下三处不便:pushState 改了栈却不发 popstate,得自己补渲染;站内 <a> 点击要手动拦截;读不到完整的历史条目列表,只有一个 history.length 数字。Navigation API
是为路由量身重做的一代——链接点击、表单提交、navigate()、前进后退,所有导航统一汇成一个 navigate 事件,用 e.intercept({ handler }) 一处接管,还能直接读 navigation.entries()。
它已进入各大浏览器:Chrome 与 Edge 102+(2022-05)、Firefox 147+(2026-01)、Safari 26.2+(2025-12),BCD 里也已不再标记为实验特性(兼容性据 MDN BCD,核对于 2026-08)。本页几个 lab 都在真实调用浏览器里的 window.navigation;浏览器偏旧时 demo 会给出提示,代码面板与对照表照常可读。
navigate 监听器。可看右侧事件日志的 navigationType 如何分类,以及左侧 navigation.entries() 实时增长。注意那条链接只是个 <a href>、没绑任何 click 监听,照样被接住。那条链接是这套 API 相对 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 |
核心是一个 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>)、还要自己防异步竞态,Navigation API 则收敛成一个 navigate 监听器,intercept 同时把 async
handler、pending 与提交时机都管了。心智模型没变,还是那套「条目加指针」的 history 栈,变的是浏览器终于给了路由一套专门设计的原生接口。
2 · 异步导航的两段时序
navigate() 与 reload() 都返回 { committed, finished } 两个 Promise:committed 在 URL 与新条目就绪时 resolve,finished 在 intercept 的 handler 跑完时 resolve。handler 收到的 e.signal 是一个
AbortSignal,下一次导航会自动 abort 上一次未完成的 handler,把它透传给 fetch 即可天然防竞态。
finished 如何 reject 出 AbortError。
两者各管一段:想在 URL 一变就更新地址栏或高亮当前导航项,等 committed;想在数据真正到位后再收尾(埋点、解除 loading),等 finished。配套的全局事件 navigatesuccess 与 navigateerror 是同一时机的文档级版本,任意一次软导航成功或失败都会派发,适合挂全局 loading
指示与错误提示。
图 0-1 里那几个控件演示的都是 History API 给不了的能力。navigate(url, { state }) 给每条历史条目挂一份 structured clone 的 state,后退与前进时 currentEntry.getState() 跟着条目回来,它不进 URL、刷新也不丢。updateCurrentEntry({ state }) 原地改当前条目的
state,栈长度不变、不新增条目,只触发 currententrychange。而那个 <form method="post"> 没绑 submit,提交照样进 navigate 事件并带着 e.formData——表单和链接是同一套入口。
3 · scroll 与 focusReset
intercept() 除了 handler 还收两个与无障碍相关的选项,默认都是 'auto',由浏览器把 SPA 软导航做得像真导航:push 与 replace 后滚到锚点或页首、traverse 与 reload 后恢复离开时的滚动位置,焦点则移到首个 [autofocus] 元素或
<body>。需要自己掌控时机(如等数据到位再滚)就设 'manual'。
| 选项 | 'auto'(默认) |
'manual' |
|---|---|---|
scroll |
push 与 replace 滚到锚点或页首;traverse 与 reload 恢复原滚动位置 | 浏览器不自动滚,由 handler 内择机调 e.scroll() |
focusReset |
导航后焦点移到首个 [autofocus],没有则 <body> |
焦点保持不动,自行管理,如保留在搜索框 |
intercept 的完整形态如下。
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 截断了前进历史 |
5 · API 速查
window.navigation 的方法、属性与条目,本页 lab 用到的都在下面。
// 方法
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 已切换,异步加载期间应立即给出占位而非空白。
守卫的取消与改道、异步导航的 pending,在 Navigation API 里分别对应 e.preventDefault() 与 intercept 的 async handler,见软导航。