← 路由设计 · 稳定入口与可变目标 / Navigation API · 一个事件接住所有导航 待审核 11 / 23

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、finishedintercept 的 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 面板尾部 n=n=\dots);后退 / 前进时 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 的权威说明与浏览器兼容性。