配了多年 SPA,今天才知道 History API 拼凑的路由从来不是标准答案——Navigation API 把这件事彻底原生化了
写过 SPA 的人都踩过这个坑——每次做页面路由都要把 History API、popstate 事件、link 点击监听、pushState 调用拼起来,少管一个边界情况用户的 back/forward 按钮就坏了。这个问题从 jQuery 时代拖到 React Router 时代,框架换了一套又一套,原生平台从来没给过标准答案。
Navigation API 就是那个答案。
Navigation API 的核心是一个 navigate 事件。这个事件在任何导航行为触发时都会 fires:链接点击、表单提交、back/forward 按钮、location.href 赋值,全部走同一条路。
以前用 History API 怎么做路由?
// 监听 back/forward
window.addEventListener('popstate', () => renderRoute(location.pathname));
// 监听链接点击,手动拦截默认行为
document.addEventListener('click', (e) => {
const link = e.target.closest('a[data-route]');
if (link) {
e.preventDefault();
history.pushState(null, '', link.href);
renderRoute(new URL(link.href).pathname);
}
});
// 初始渲染
renderRoute(location.pathname);
三个独立的关注点,一套手动拼起来的胶水代码。漏了哪个边界,back/forward 就坏给你看。
用 Navigation API 怎么做?
navigation.addEventListener('navigate', (event) => {
if (!event.canIntercept) return;
event.intercept({
async handler() {
const url = new URL(event.destination.url);
renderRoute(url.pathname);
},
scroll: 'manual'
});
});
一个事件,一个拦截点。URL 更新、历史栈管理、焦点恢复、 accessibility 全部由浏览器自动处理,开发者只需要写渲染逻辑。
scroll 手动控制是 SPA 的关键
Navigation API 里的 event.scroll() 解决了一个经典问题:默认情况下,浏览器在调用 event.intercept() 后立即恢复滚动位置。但在 SPA 里,内容往往是异步加载的——等你 API 数据回来,页面高度还不够,滚动位置早就定在错误的地方了。
event.intercept({
scroll: 'manual',
async handler() {
const data = await fetchPageData();
renderContent(data);
// 数据渲染完了,内容高度够了,再恢复滚动位置
event.scroll();
}
});
这样 back/forward 按钮返回时,滚动位置才会在正确时机被恢复,而不是在旧内容还在显示时就跳走。
History API 从来没有的能力
Navigation API 还给了开发者一些 History API 根本做不到的能力:
navigation.entries() 可以读取同源的全部历史记录条目,entry.getState() 可以读写每个条目的结构化状态,navigation.traverseTo(key) 可以直接跳转到任意历史条目。这些能力对于复杂 SPA 的状态管理、多步表单流程、编辑器类的「Undo/Redo」场景非常重要,History API 只能操作当前条目,其他条目完全黑盒。
表单提交不用 reload
navigate 事件还自动拦截同文档的表单提交,通过 event.formData 暴露表单数据,让你用异步逻辑处理标准 HTML 表单提交,无需 page reload。这是 <form> 标签从后端表单时代向 SPA 迁移的最后一块拼图。
和 View Transitions API 无缝衔接
Navigation API 和 View Transitions API 是天生一对。拦截导航时,把 DOM 更新包在 document.startViewTransition() 里,浏览器会自动在旧状态和新状态之间播放视图过渡动画,让 SPA 也能有原生应用级别的切换动效。
const url = new URL(event.destination.url);
event.intercept({
async handler() {
const content = await fetchContent(url.pathname);
document.startViewTransition(() => {
document.getElementById('app').innerHTML = content;
});
}
});
浏览器支持:全平台 Baseline 2026
Navigation API 在 2026 年 1 月正式进入 Baseline,四大浏览器全部支持:Chrome/Edge 117+、Firefox 147+、Safari 26.2+。这意味着生产环境可以直接用,不需要 polyfill,不需要框架路由来处理 History API 兼容。
框架层面:React Router 和 TanStack Router 都在讨论如何将 Navigation API 作为底层,但 Navigation API 解决的不是框架层面的问题——它给平台补上了一个 web 从诞生起就缺失的底层原语:用事件中心化的方式处理任意导航行为。Jake Archibald(Chrome 团队 web 平台工程师)说:「The web now has sensible, low-level routing for navigations.」
下一步
把你的 SPA 路由从 History API 迁移到 Navigation API 只需要几十行代码,但收益是显著减少的边界情况 bug 和更可靠的用户体验。如果你的路由需求不复杂(内容站、文档站、控制台),现在完全可以不引入任何路由库,让平台来处理这件事。
评论区
登录后可评论。