SPA路由我写了八年,今天终于不用自己处理那些坑了——Navigation API 实际迁移指南
做过大型 SPA 的同学都有同感:用 History API 写路由,代码越写越乱——同一个页面刷新后 URL 对不上、popstate 事件莫名其妙不触发、多个标签页之间状态不同步……这些问题你可能已经习以为常了,但 Navigation API 把它们全原生解决了。我花了两周把手头的项目从 History API 迁过去,把中间踩的坑和迁移步骤全拆清楚了。
History API 那几个坑,你肯定遇到过
用 History API 做 SPA 路由,最常见的几个问题:
popstate 事件在 Safari 和 Chrome 行为不一样。 Chrome 只在前进/后退时触发,pushState 和 replaceState 不会触发;但 Safari 上有些版本表现又不同。这种不一致导致很多人干脆自己维护一个状态对象来跟踪路由,而不是依赖浏览器事件。
页面刷新之后,URL 和实际状态可能对不上。 比如用户点了”下一页”,你 pushState 了,但刷新页面之后服务器端没有对应的路由处理,SPA 就会 404。手写这套 fallback 逻辑很容易出错。
多个标签页之间无法同步状态。 用户在 A 标签页点了导航,B 标签页不知道发生了什么。你需要配合 storage 事件自己写一套广播逻辑,代码量不小。
这些问题的根源是 History API 设计出来的时候 SPA 根本不是主流场景,它只是对服务器端路由的浏览器端补充。
Navigation API 怎么解决
Navigation API 的核心设计思路是”一个事件处理所有导航”。不管你是点链接、表单提交、浏览器前进后退、还是代码主动触发,全部通过一个 navigate 事件分发:
navigation.addEventListener('navigate', (navigateEvent) => {
const url = new URL(navigateEvent.destination.url);
// 统一在这里处理,不需要分别写 popstate / pushState / link click
if (shouldNotIntercept(navigateEvent)) return;
if (url.pathname === '/') {
navigateEvent.intercept({ handler: loadIndexPage });
} else if (url.pathname === '/cats/') {
navigateEvent.intercept({ handler: loadCatsPage });
}
});
intercept({ handler }) 的 handler 是什么?就是你的页面渲染函数。框架可以在这里接管渲染,SSR 也可以在这里处理。更重要的是,Navigation API 会自动管理 navigation.transition 对象,让你在任何地方都能查询”现在正在导航吗,导航到哪了”:
// 任何地方都能查导航状态,不需要自己维护状态
const transition = navigation.transition;
if (transition) {
transition.finished.then(() => {
console.log('导航完成,可以更新 UI 状态');
});
}
页面刷新后 URL 和状态不一致这个问题,Navigation API 通过 navigation.currentEntry 直接给你当前入口的状态,包括 key(用于判断是否是同一次导航的返回)和 getState()(你的自定义状态):
const entry = navigation.currentEntry;
const state = entry.getState(); // 刷新后状态不丢
const key = entry.key; // 可以判断是不是同一次访问
多个标签页同步问题,Navigation API 配合 window.name 或者 BroadcastChannel API 可以更干净地处理,但说实话这个场景现在有更标准的 Service Worker 方案。
迁移步骤:一步一步来
我从自己的项目里总结了一套迁移步骤,不需要一次性全部重写:
第一步:保留 History API,新增 Navigation 监听。 在现有路由逻辑旁边加一个 navigate 事件监听,先只打日志,看看有哪些导航类型会触发。这样你可以评估现有代码的覆盖情况。
navigation.addEventListener('navigate', (e) => {
console.log('Navigation API 捕获:', e.destination.url, e.navigationType);
});
第二步:把主动触发从 pushState 迁移到 navigate。 History API 的 pushState(state, title, url) 对应 Navigation API 的 navigation.navigate(url)。后者返回的是 NavigationHistoryEntry,并且支持 data 和 title 参数:
// 旧写法
history.pushState({ page: 'cats' }, '', '/cats/');
// 新写法
navigation.navigate('/cats/', { state: { page: 'cats' } });
第三步:把 popstate 监听替换成 transition 监听。 前进/后退的处理是最关键的迁移点。History API 需要在 popstate 里手动判断 entry 是从哪来的;Navigation API 的 navigation.back() 和 navigation.forward() 返回一个 promise,可以直接 await:
// 旧写法
window.addEventListener('popstate', (e) => {
if (e.state?.page === 'cats') renderCats();
});
// 新写法
navigation.addEventListener('navigate', (e) => {
if (e.navigationType === 'traverse') {
// 用户在历史记录间切换
e.intercept({ handler: handleTraverse });
}
});
async function handleTraverse() {
const entry = navigation.currentEntry;
const state = entry.getState();
// 根据 state 渲染对应页面
}
第四步:处理刷新后状态恢复。 History API 刷新后 state 会丢失,需要在应用入口重新读取。Navigation API 的 navigation.currentEntry 在应用启动时就能拿到,刷新前后是同一个 entry:
// 应用入口
const current = navigation.currentEntry;
if (current) {
// 刷新后直接恢复状态,不需要额外的恢复逻辑
renderPage(current.getState());
}
// 监听首次加载
navigation.addEventListener('navigatesuccess', (e) => {
console.log('页面加载完成');
});
浏览器支持情况
截至 2026 年 8 月,Navigation API 已经进入 Baseline 2024,所有主流浏览器都支持。你不需要 polyfill 就能在生产环境用。但需要注意几点:
Safari 支持是在 16.4 之后,如果你的项目需要兼容更老的 Safari 版本,可能需要做特性检测:
if ('navigation' in window) {
// 用 Navigation API
} else {
// 降级到 History API
}
另外,如果你用了 <a rel="noopener"> 或者手动 target="_blank",这些链接不会触发 navigate 事件,这是预期行为。
结论
从 History API 迁到 Navigation API 不是”功能升级”,是”架构改善”。核心价值有三个:
一是你再也不用在 popstate、link click、form submit 之间各自写一套重复的路由逻辑了,navigate 事件是唯一入口。二是页面状态不再依赖浏览器的不可靠行为,刷新、前进、后退全部有标准接口。三是框架和库可以基于这个 API 构建更可靠的路由实现,而不需要自己发明一套状态管理。
迁移不需要一次性全部重写,按上面的四步走,每次迭代一个模块,两周就能把一个中型 SPA 迁完。最大的收益是你会发现之前为了绕 History API 的坑写的那些 workaround 代码,全部可以删掉。
评论区
登录后可评论。