こうこく
作 ▸
改 ▸

history.pushStateとpopstateを試してみる

今時のWebサービス使ってると、ページがリロードされてないのにURLがいつの間にか変わってたり、なのになぜかブラウザバックで適切な箇所に戻れたりするアレ。

SPAのこと調べてて、次のスライド見てました。

まだ DOM 操作で消耗してるの?

それで、初めて history.pushState() の存在を知りました。これ使うと、ページ遷移しなくてもブラウザの「戻る」の履歴を増やせるらしいです。そして実際に「戻る」が押されたときには、popstate イベントを使ってページの状態をハンドリングできるみたいです。

ブラウザの履歴を操作する - ウェブデベロッパーガイド | MDN

↑のページ読みながら、サンプル書いてみました。

See the Pen pushstate-and-popstate by napoporitataso (@napoporitataso) on CodePen.

ここではあらかじめHTMLに全ての画面の内容を書いておき、リンクが踏まれた時に href を見て、画面に対応する要素の表示/非表示を切り替えているだけです。state には画面の識別子を持たせており、どの画面を表示するかはそれで判断しています。

HTML
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="robots" content="noarchive" />
    <title>pushState / popState サンプル</title>
  </head>
  <body>
    <p>現在のURL: <span id="url"></span></p>
    <div data-screen="start">
      <h1>スタート</h1>
      <p>リンクを踏むと、リロードなしでページの内容とURLが切り替わります。</p>
      <p>ブラウザで「戻る」「進む」もできます。</p>
      <a href="#p1">次→</a>
    </div>

    <div data-screen="p1">
      <h1>その1</h1>
      <a href="#start">←前</a> <a href="#p2">次→</a>
    </div>

    <div data-screen="p2">
      <h1>その2</h1>
      <a href="#p1">←前</a> <a href="#p3">次→</a>
    </div>

    <div data-screen="p3">
      <h1>その3</h1>
      <a href="#p2">←前</a>
    </div>

    <script src="script.js"></script>
  </body>
</html>
JavaScript
'use strict';

/**
 * 画面を表示する
 * @param {string} screen 画面の識別子
 */
const showScreen = (screen) => {
  // 遷移先の画面に対応する要素だけを表示し、それ以外は非表示に
  document.querySelectorAll('[data-screen]').forEach((elem) => {
    if (elem.dataset.screen === screen) {
      elem.style.display = 'block';
    } else {
      elem.style.display = 'none';
    }
  });
};

/**
 * 現在のURLを表示する
 */
const showUrl = () => {
  document.querySelector('#url').textContent = document.location.toString();
};

// hrefが#から始まるaタグがクリックされたとき
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
  anchor.addEventListener('click', (e) => {
    e.preventDefault(); // aタグ本来の画面遷移をキャンセル

    // 対象の画面を表示
    const screen = e.currentTarget.hash.substr(1);
    showScreen(screen);

    // https://developer.mozilla.org/ja/docs/Web/API/History/pushState
    // @param {any} 「戻る」「進む」が押されたときに受け取りたいデータ (state)
    // @param {string} 空文字固定
    // @param {string} 遷移先のURL
    history.pushState({ screen }, '', `./${screen}`);

    showUrl();
  });
});

// ブラウザの「戻る」「進む」が押されたとき
window.addEventListener('popstate', (e) => {
  // e.stateにはpushStateの第1引数にセットした値が入ってる
  // セットされていなければ最初の画面を表示、セットされていればその画面を表示
  if (e.state === null) {
    showScreen('start');
  } else {
    showScreen(e.state.screen);
  }
  showUrl();
});

// 初期表示
showScreen('start');
showUrl();

Note

なお、これだけだとブラウザで画面をリロードしたりURLを直接指定してアクセスした時に、URLに対応するファイルが実在しないので画面を表示できません。

そのためサーバー側で .htaccess などを使用して全てのアクセスを index.html に集めるといった、フォールバック的な動きをさせる必要があります。

.htaccessのサンプル
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.html [L]
この記事に何かあればこちらまで (非公開)