Navigation API 正式成为 Web 基线——SPA 路由终于有了正经方案

2026-05-18 17 预计阅读时间:1 分钟
来源:infoq.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:9 分钟

从 2026 年 1 月起,Navigation API 在主流浏览器全部可用,正式进入 Baseline。这意味着单页应用里那套拼凑 history.pushState + popstate + 各种补丁的路由方案,终于可以被一个统一、有错误处理、有事件模型的浏览器原生 API 替代。

History API 的痛点有多真实

写过 SPA 跪过路由的人都知道:window.history 本来是为"前进/后退"设计的,不是为应用路由设计的。用它做路由,你会撞上这些问题:

  • popstate 只管后退,不管前进——pushStatereplaceState 不触发任何事件,你得自己手动同步状态。
  • 没有统一的拦截点——想在导航前做权限检查或数据预加载?只能在各处散落的手动调用里加逻辑,漏一个就是 bug。
  • 错误处理靠自觉——导航失败后 URL 已经变了,但页面没渲染,用户看到空白或旧内容,没有回滚机制。
  • hashchangepopstate 混用——两种事件各管各的,调试时事件顺序难以预测。

Navigation API 把这些痛点一次性解决:一个 navigate 事件覆盖所有导航触发方式,URL 更新和状态管理由 API 统一负责,导航失败可以自动回滚。

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 函数,数据加载期间浏览器会显示过渡动画(如果设置了 focusResetscrollRestoration)。
  • 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,不是简单换个函数名。以下是关键步骤:

  1. 删除所有 popstatehashchange 监听——统一用 navigation.addEventListener('navigate', ...) 替代。
  2. 删除所有 history.pushState / history.replaceState 调用——改用 navigation.navigate(url, { history: 'push' }){ history: 'replace' })
  3. 把散落的"改 URL + 渲染"合并到拦截器——intercepthandler 是唯一的渲染入口,别在别处再手动渲染。
  4. 处理首次加载——页面初始化时单独调用一次渲染逻辑,navigate 事件不覆盖这个场景。
  5. 跨域链接不用管——不调用 intercept() 时浏览器走默认行为,外部链接正常跳转。
  6. 渐进增强——检测 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 对象是独立的,一个标签页的导航不会触发另一个标签页的事件。需要跨标签页通信仍得用 BroadcastChannelstorage 事件。
  • navigate 事件不能阻止下载和表单提交——这些不属于"导航",API 不干预。
  • 和第三方路由库的关系——React Router、Vue Router 等库已经开始实验性支持 Navigation API 作为底层驱动,但目前多数还是用 History API。如果你重度依赖某个路由库,等库官方支持再迁移,比自己绕开库直接用 API 更稳。

一句话总结:Navigation API 让 SPA 路由从"凑合能用"变成"有规范可依"。新项目可以直接用它替代 History API;老项目等路由库适配后迁移最省力。


相关推荐