Navigation APIでSPAルーターを作る|History APIとの違いとフォールバック

Navigation API。SPAのページ遷移を扱う記事のアイキャッチ Web標準API
カテゴリー
Web標準API
公開日
2026.09.26

はじめに

従来の小さなSPAルーターは、リンクのclick、history.pushState()、popstateを別々に扱う必要がありました。Navigation APIは、ナビゲーションをnavigateイベントへ集約し、intercept()内で描画処理を実行できます。

この記事ではHomeとAboutを切り替える最小ルーターを作ります。フレームワークのルーターを置き換えることが目的ではなく、Web標準の動作を理解する記事です。

SPAとルーターとは

SPA(Single Page Application)は、リンクを押すたびにHTML全体を読み直すのではなく、JavaScriptで必要な表示だけを切り替えるWebアプリです。ルーターは、/や/aboutというURLと表示内容を対応付けます。

HTMLとJavaScriptの基本を学び、ファイル作成とターミナル操作ができる方向けです。完成後は、HomeとAboutをクリックするとURLと本文が切り替わり、ブラウザの戻る・進むも使えます。Navigation API非対応時は通常のページ遷移へ戻します。ただし本文の描画にはJavaScriptが必要です。JavaScript無効時にも本文を届けるには、サーバー側で各ページのHTMLを返す別の仕組みが必要です。

必要なファイルと起動方法

Node.js 22.12以上(この例では24系)とnpm、Chromeなどのブラウザを用意します。同じフォルダーにindex.htmlとapp.jsを作り、開発サーバーから配信します。

mkdir navigation-demo
cd navigation-demo
npm init -y
npm install --save-dev vite@7.3.6

下記の2ファイルを保存したらnpx viteを実行し、表示されたローカルURL(通常はhttp://localhost:5173)を開きます。終了はターミナルでCtrl+Cです。Viteの開発サーバーはSPAのフォールバックに対応します。本番配信でも/aboutへindex.htmlを返す設定が必要です。

HTML

<!doctype html>
<html lang="ja">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Navigation API Demo</title>
<style>
  body { max-width: 720px; margin: 32px auto; padding: 0 16px; font-family: system-ui; }
  nav { display: flex; gap: 16px; }
</style>
<nav>
  <a href="/">Home</a>
  <a href="/about">About</a>
</nav>
<main id="app"><p>ページを読み込んでいます。</p></main>
<noscript><p>このデモの本文表示にはJavaScriptが必要です。</p></noscript>
<p id="error" role="alert"></p>
<script type="module" src="/app.js"></script>
</html>

開発サーバーは/aboutを直接開いても同じHTMLを返すように設定します。これを行わないと、再読み込み時に404になります。

ルーター

次をapp.jsとして保存します。innerHTMLへ入れるのは、コード内で定義した固定文字列だけです。URLの値や利用者入力をそのまま連結するとXSSの原因になります。

const app = document.querySelector("#app");
const errorBox = document.querySelector("#error");

const routes = new Map([
  ["/", () => "<h1>Home</h1><p>トップページです。</p>"],
  ["/about", () => "<h1>About</h1><p>このサイトについて。</p>"],
]);

async function render(url, signal) {
  signal?.throwIfAborted();
  const view = routes.get(url.pathname);
  app.innerHTML = view ? view() : "<h1>404</h1>";
  document.title = view ? `Demo | ${url.pathname}` : "404 | Demo";
}

// 初回ロードではnavigateイベントが発生しないため、明示的に描画する。
await render(new URL(location.href));

if ("navigation" in window) {
  navigation.addEventListener("navigate", (event) => {
    const destination = new URL(event.destination.url);

    if (!event.canIntercept) return;
    if (destination.origin !== location.origin) return;
    if (event.hashChange || event.downloadRequest !== null) return;
    if (event.formData !== null || event.navigationType === "reload") return;

    event.intercept({
      async handler() {
        errorBox.textContent = "";
        try {
          await render(destination, event.signal);
        } catch (error) {
          if (error.name !== "AbortError") {
            errorBox.textContent = "画面の読み込みに失敗しました。";
            throw error;
          }
        }
      },
    });
  });
}

別ページへの遷移が始まると前のevent.signalがabortされることがあります。非同期取得を追加する場合はfetch(url, { signal: event.signal })へ渡し、取得後・描画直前にもsignal.throwIfAborted()を呼びます。この例の描画は固定文字列で、非同期通信はありません。フォーム送信と再読み込みは横取りせず、元のブラウザ動作に任せます。

非対応ブラウザのフォールバック

Navigation APIがないブラウザでもJavaScript自体が動けば、通常遷移でサーバーからHTMLを読み直し、初回のrender()が本文を描画します。これは「JavaScript不要」という意味ではありません。以下は分岐を理解するための補足で、完成コードに追加しなくても通常遷移へ戻ります。

if (!("navigation" in window)) {
  console.info("通常のページ遷移を使用します");
}

リンクをhref="#"にしてJavaScriptだけで動かす設計は、非対応時やスクリプト失敗時に壊れるため避けます。

動作確認

  • リンククリックでページ全体を再読込せず描画が変わる
  • 戻る・進むで表示とURLが一致する
  • /aboutを直接開いて表示できる
  • 外部リンクとdownloadリンクを横取りしない
  • 高速に連打しても古い描画が残らない
  • 不明なURLで404表示になる

期待結果は、Homeで「トップページです。」、Aboutで「このサイトについて。」です。/missingでは画面に404と出ますが、SPAフォールバックが返すHTTPステータスは200です。本番で正しいHTTP 404を返すにはサーバー側のルーティングも必要です。非対応分岐を試すときは検証用コピーで条件をif (false)へ置き換え、リンクが通常遷移して表示が復元するか確かめます。これは非対応ブラウザ実機の検証とは区別します。

制約

初回ロードではnavigateイベントが発生しないため初期描画が必要です。APIが扱う履歴は現在のbrowsing contextかつ同一オリジンの範囲です。SSR、データキャッシュ、ネストしたroute、認証guardまで必要なアプリでは、成熟したルーターライブラリの方が適する場合があります。

よくあるトラブル

  • リンクを押すとページ全体が再読み込みされる:ブラウザがNavigation APIへ対応しているか、コンソールに例外がないか確認する
  • /aboutを再読み込みすると404になる:開発・配信サーバーのSPAフォールバックを設定する
  • 戻る操作で本文が変わらない:navigateイベント内で同じrender()を呼んでいるか確認する
  • 外部リンクまで横取りする:遷移先のoriginを確認してからintercept()する
  • 画面を連打すると古い内容が出る:event.signalをfetch()や描画処理へ渡す

関連記事

まとめ

Navigation APIは、SPAのナビゲーション入口をnavigateへまとめます。canIntercept、origin、hash、downloadを確認し、AbortSignalと初回描画を忘れないことが最小実装の要点です。

参考リンク

コメント

タイトルとURLをコピーしました