# Navigation API

> window.navigation 与 navigate 事件如何让单页应用拦截并控制浏览器导航，以及 Chrome、Firefox、Safari 的支持版本。

**一句话：** 根据 MDN 的说明，"Navigation API 提供了发起、拦截和管理浏览器导航操作的
能力"——它是 History API 与 `window.location` 的继任者，专门面向单页应用（SPA）的需求。

## 这是什么

该 API 通过 `Window.navigation` 访问，根据 MDN 的说明，它"返回一个全局 `Navigation`
对象的引用"——每个 `window` 都有自己独立的实例。其中最重要的事件是 `navigate`，根据
MDN 的 Navigation API 概览页面，该事件"在任意类型的导航发起时触发，这意味着你可以从
一个中心位置控制所有页面导航，非常适合 SPA 框架中的路由功能"。MDN 的 `navigate_event`
参考文档对同一事件的描述更为具体："在任意类型的导航发起时触发，使你能够按需拦截"。

## 支持情况

根据 MDN 的 browser-compat-data，`Navigation` 接口自 Chrome 102 起受支持（Chrome for
Android 同样自 102 起），自 Firefox 147 起受支持，自 Safari 26.2 起受支持（Safari on
iOS 同样自 26.2 起）——兼容性数据中 Edge、Opera、Samsung Internet 的条目标记为
`"mirror"`，意味着它们的支持数据是根据各自对应的上游 Chromium 版本推算得出的，而不是
直接给出的数值；这并不代表这些浏览器与 Chrome 使用相同的版本号。
`NavigateEvent` 接口本身与之同期上线，同样自 Chrome 102 起；只有其当前名称的
`intercept()` 方法稍晚才出现，即 Chrome 105（此前存在一个名称不同的方法
`transitionWhile`，自 Chrome 102 起存在，直到 Chrome 108 被移除）。Firefox 147 与
Safari 26.2 则是将 `NavigateEvent` 与 `intercept()` 一并上线。

## 如何使用

监听 `navigate` 事件并调用 `intercept()`，用自定义逻辑处理导航而不是整页加载。以下示例
假设页面的应用外壳（app shell）已经渲染了一个 `<div id="article"></div>` 容器供其更新：

```js
navigation.addEventListener("navigate", (event) => {
  // 有些导航（例如跨源导航）无法被拦截——让浏览器按常规方式处理。
  if (!event.canIntercept) {
    return;
  }
  // 不拦截 hash 导航或下载。
  if (event.hashChange || event.downloadRequest !== null) {
    return;
  }

  const url = new URL(event.destination.url);

  event.intercept({
    async handler() {
      renderArticlePagePlaceholder();
      const articleContent = await getArticleContent(url.pathname);
      renderArticlePage(articleContent);
    },
  });
});

function renderArticlePagePlaceholder() {
  document.querySelector("#article").innerHTML = "<p>加载中…</p>";
}

async function getArticleContent(pathname) {
  const response = await fetch(`/api/articles${pathname}`);
  return response.text();
}

function renderArticlePage(html) {
  document.querySelector("#article").innerHTML = html;
}
```

根据 MDN 的 `navigate_event` 参考文档，调用 `handler()` 时 URL 已经更新，因此其自身
示例会先渲染占位内容，再在获取到真实内容后替换——上面展示的正是这一模式。

## 运行时检测与回退

在注册处理函数之前，需要同时检测 `window` 上的 `navigation` 与
`NavigateEvent.prototype.intercept`——根据上文的兼容性数据，`window.navigation` 从
Chrome 102 就存在，但 `intercept()` 直到 Chrome 105 才上线，因此只检测 `window` 会让
Chrome 102–104 误判为支持，并在首次导航时抛出 `TypeError`。不支持时应回退为真正可用的
链接跳转与 History API 导航。以下示例假设页面已存在 `<div id="article"></div>` 容器，
以及若干站内链接（例如 `<a href="/articles/1">`）：

```js
function supportsNavigationApi() {
  return (
    "navigation" in window &&
    typeof NavigateEvent !== "undefined" &&
    typeof NavigateEvent.prototype.intercept === "function"
  );
}

if (supportsNavigationApi()) {
  navigation.addEventListener("navigate", handleAppNavigation);
} else {
  // 此处没有可用的 Navigation API——依赖常规的链接点击以及
  // History API 的 pushState/popstate 来实现路由。
  setupHistoryApiRouting();
}

function handleAppNavigation(event) {
  if (
    !event.canIntercept ||
    event.hashChange ||
    event.downloadRequest !== null ||
    typeof event.intercept !== "function"
  ) {
    return;
  }
  const url = new URL(event.destination.url);
  event.intercept({
    async handler() {
      const response = await fetch(`/api/articles${url.pathname}`);
      document.querySelector("#article").innerHTML = await response.text();
    },
  });
}

function setupHistoryApiRouting() {
  // 拦截同源链接点击，改为推入新的 History 记录，而不是触发整页加载。
  document.addEventListener("click", (event) => {
    const link = event.target.closest("a[href]");
    if (
      !link ||
      link.origin !== location.origin ||
      event.defaultPrevented ||
      event.button !== 0 ||
      event.metaKey ||
      event.ctrlKey ||
      event.shiftKey ||
      event.altKey
    ) {
      return;
    }
    event.preventDefault();
    history.pushState(null, "", link.href);
    renderArticleFromPath(location.pathname);
  });

  // 前进/后退导航时，渲染与地址栏匹配的文章。
  window.addEventListener("popstate", () => {
    renderArticleFromPath(location.pathname);
  });

  renderArticleFromPath(location.pathname);
}

async function renderArticleFromPath(pathname) {
  const response = await fetch(`/api/articles${pathname}`);
  document.querySelector("#article").innerHTML = await response.text();
}
```

## 实用清单

- 根据 MDN 的 `navigate_event` 参考文档，应"在不该被拦截的导航上提前退出"——例如
  `event.hashChange` 或 `event.downloadRequest` 的情况——并非所有 `navigate` 事件都
  应该被拦截。
- Chrome 的 `intercept()` 方法取代了此前一个已被移除的方法 `transitionWhile`——针对
  Chrome 102–107 编写的、使用 `transitionWhile` 的示例代码在当前浏览器上无法运行。
- 根据 MDN 的兼容性数据，`NavigateEvent`/`intercept()` 分别在 Firefox 147 与
  Safari 26.2 才落地，明显晚于 Chrome——站点仍需要为低于这些版本的浏览器保留
  History API 回退路径。
- 根据 MDN 的说明，`navigate` 事件是所有导航类型的中心事件，但并非所有导航都能被
  拦截：根据 MDN 的 `NavigateEvent.canIntercept` 参考文档，跨源导航就是 `canIntercept`
  为 `false` 的情况之一，因此处理函数必须检查它，并让浏览器按常规方式处理这类导航，
  而不是尝试拦截它们。

## 相关参考

- [Document Picture-in-Picture：置顶窗口](/zh/reference/capabilities/document-picture-in-picture/)
- [Window Management](/zh/reference/capabilities/window-management/)