从 2026 年 1 月起,Navigation API 在主流浏览器全部可用,正式进入 Baseline。这意味着单页应用里那套拼凑 history.pushState + popstate + 各种补丁的路由方案,终于可以被一个统一、有错误处理、有事件模型的浏览器原生 API 替代。
History API 的痛点有多真实
写过 SPA 跪过路由的人都知道:window.history 本来是为"前进/后退"设计的,不是为应用路由设计的。用它做路由,你会撞上这些问题:
popstate只管后退,不管前进——pushState和replaceState不触发任何事件,你得自己手动同步状态。- 没有统一的拦截点——想在导航前做权限检查或数据预加载?只能在各处散落的手动调用里加逻辑,漏一个就是 bug。
- 错误处理靠自觉——导航失败后 URL 已经变了,但页面没渲染,用户看到空白或旧内容,没有回滚机制。
hashchange和popstate混用——两种事件各管各的,调试时事件顺序难以预测。
Navigation API 把这些痛点一次性解决:一个 navigate 事件覆盖所有导航触发方式,URL 更新和状态管理由 API 统一负责,导航失败可以自动回滚。
navigate 事件:一个拦截点管所有入口
navigation.navigate()、用户点击链接、用户按后退/前进、location.href 赋值——所有这些操作都会触发同一个 navigate 事件。你只需要在一个地方监听:
navigation.addEventListener('navigate', (event) => {
// event.destination 包含目标 URL 和状态
const { url, state } = event.destination;
// 需要拦截?调用 intercept(),不调用则走浏览器默认行为
event.intercept({
async handler() {
// 这里做你的路由逻辑:权限检查、数据预加载、渲染新页面
if (!hasPermission(url.pathname)) {
event.commit(); // 先把 URL 回滚到当前地址
showForbiddenPage();
return;
}
await preloadPageData(url.pathname);
renderPage(url.pathname);
},
// 导航失败(handler 抛异常)时的回滚
errorCallback(err) {
console.error('导航失败:', err);
showErrorMessage(err.message);
},
});
});
几个关键细节:
intercept()是异步的——handler可以是 async 函数,数据加载期间浏览器会显示过渡动画(如果设置了focusReset和scrollRestoration)。event.commit()——手动确认 URL 更新。默认在handler完成后自动 commit;如果中途失败,URL 会自动回滚到导航前的地址。- 不调用
intercept()——浏览器按默认行为处理(比如跨域链接正常跳转),你的代码不需要对"不归你管"的导航做特殊处理。
主动导航:取代 pushState / replaceState
旧写法:
// History API:改 URL 不触发事件,得自己手动渲染
history.pushState({ page: 'detail' }, '', '/detail/42');
renderDetailPage(42);
新写法:
// Navigation API:navigate 事件会自动触发,拦截器里统一处理
navigation.navigate('/detail/42', {
state: { page: 'detail' },
history: 'push', // 'push' 对应 pushState,'replace' 对应 replaceState
});
区别很明显:你不再需要"改 URL + 手动渲染"两步操作。调用 navigate() 后,navigate 事件的拦截器会接管渲染逻辑,URL 和页面状态始终同步。
完整 SPA 路由示例
下面是一个可以直接跑的最小 SPA 路由,用 Navigation API 实现,不需要任何第三方路由库:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Navigation API SPA Demo</title>
<style>
nav a { margin-right: 12px; }
#app { padding: 24px; font-size: 18px; }
.loading { opacity: 0.5; }
</style>
</head>
<body>
<nav>
<a href="/">首页</a>
<a href="/about">关于</a>
<a href="/detail/42">详情 #42</a>
</nav>
<div id="app"></div>
<script>
const app = document.getElementById('app');
// 模拟异步数据加载
async function fetchPageData(path) {
// 实际项目中替换为真实 API 调用
await new Promise(r => setTimeout(r, 300));
return { title: `页面: ${path}`, content: `这是 ${path} 的内容` };
}
// 渲染函数
function render(data) {
app.textContent = '';
const h1 = document.createElement('h1');
h1.textContent = data.title;
const p = document.createElement('p');
p.textContent = data.content;
app.append(h1, p);
}
// 核心:一个拦截器管所有路由
navigation.addEventListener('navigate', (event) => {
const url = new URL(event.destination.url, location.origin);
// 只处理同源且非 hash-only 的导航
if (url.origin !== location.origin) return;
event.intercept({
async handler() {
app.classList.add('loading');
try {
const data = await fetchPageData(url.pathname);
render(data);
} finally {
app.classList.remove('loading');
}
},
errorCallback(err) {
app.classList.remove('loading');
render({ title: '出错了', content: err.message });
},
});
});
// 首次加载:手动触发当前 URL 的渲染
// Navigation API 的 navigate 事件不会在页面初始加载时触发
fetchPageData(location.pathname).then(render);
</script>
</body>
</html>
把这段代码保存为 index.html,用本地服务器打开(python -m http.server 即可),点击导航链接观察:URL 变化、内容异步加载、加载期间有 loading 状态,失败时显示错误信息——全部由一个 navigate 监听器完成。
注意:首次页面加载不会触发
navigate事件,你仍需手动初始化当前路由。这是有意为之的设计——初始加载属于文档生命周期,不属于导航。
迁移检查清单
从 History API 迁移到 Navigation API,不是简单换个函数名。以下是关键步骤:
- 删除所有
popstate和hashchange监听——统一用navigation.addEventListener('navigate', ...)替代。 - 删除所有
history.pushState/history.replaceState调用——改用navigation.navigate(url, { history: 'push' })或{ history: 'replace' })。 - 把散落的"改 URL + 渲染"合并到拦截器——
intercept的handler是唯一的渲染入口,别在别处再手动渲染。 - 处理首次加载——页面初始化时单独调用一次渲染逻辑,
navigate事件不覆盖这个场景。 - 跨域链接不用管——不调用
intercept()时浏览器走默认行为,外部链接正常跳转。 - 渐进增强——检测
window.navigation是否存在,不存在时 fallback 到旧方案或第三方路由库:
if (window.navigation) {
// 用 Navigation API
navigation.addEventListener('navigate', handleNavigate);
} else {
// fallback: 用 History API + popstate
window.addEventListener('popstate', handlePopstate);
}
边界和风险
Navigation API 不是万能药,有几个需要注意的点:
- 浏览器版本门槛——2026 年 1 月才进入 Baseline,如果你的用户群里有大量旧浏览器,必须准备 fallback。
- 不支持多标签页同步——不同标签页的
navigation对象是独立的,一个标签页的导航不会触发另一个标签页的事件。需要跨标签页通信仍得用BroadcastChannel或storage事件。 navigate事件不能阻止下载和表单提交——这些不属于"导航",API 不干预。- 和第三方路由库的关系——React Router、Vue Router 等库已经开始实验性支持 Navigation API 作为底层驱动,但目前多数还是用 History API。如果你重度依赖某个路由库,等库官方支持再迁移,比自己绕开库直接用 API 更稳。
一句话总结:Navigation API 让 SPA 路由从"凑合能用"变成"有规范可依"。新项目可以直接用它替代 History API;老项目等路由库适配后迁移最省力。