●popstateイベントとは
Webアプリケーションの開発において、ユーザーがブラウザの戻るボタンや進むボタンを使って画面遷移した際に、JavaScriptでその動作を検知したいというニーズは非常に多いと思います。
そんな時に活躍するのが、popstateイベントです。
○popstateの仕様と動作
popstateイベントは、HTML5で追加されたAPIの一つで、履歴エントリの変化を検出するために使用されます。
具体的には、history.pushState()やhistory.replaceState()によって履歴エントリが追加・変更された場合や、ブラウザの戻る/進むボタンが押された場合に発火します。
popstateイベントには、state プロパティが含まれており、これは pushState()やreplaceState()の第1引数で指定したstateオブジェクトを参照します。
つまり、履歴エントリに関連付けられたアプリケーション固有の状態を、popstateイベントを通じて取得できるわけです。
○popstateが発火するタイミング
では、popstateイベントはどのようなタイミングで発火するのでしょうか。
主なケースは次の3つです。
- history.pushState()またはhistory.replaceState()が呼び出された時
- ブラウザの戻る/進むボタンが押された時
- location.hash の変更によりハッシュ遷移が発生した時
ただし、popstateイベントはページ読み込み時には発火しないので注意が必要です。
また、iframeの中で発生したpopstateイベントは、親ウィンドウには伝播しません。
○サンプルコード1:popstateの基本的な使い方
popstateイベントの基本的な使い方は、次のようなコードになります。
window.addEventListener('popstate', function(event) {
console.log('popstateイベントが発火しました');
console.log('現在のURL:', location.href);
console.log('stateオブジェクト:', event.state);
});
// 履歴エントリの追加
history.pushState({ page: 1 }, '1ページ目', '?page=1');
history.pushState({ page: 2 }, '2ページ目', '?page=2');
// 履歴をさかのぼる
history.back();
実行結果
popstateイベントが発火しました
現在のURL: http://example.com/?page=1
stateオブジェクト: {page: 1}
pushState()で新しい履歴エントリを追加した後、history.back()で1つ前の履歴に戻ると、popstateイベントが発火し、登録していたイベントリスナーのコールバック関数が呼び出されます。
このコールバック関数の中で、現在のURLやstateオブジェクトを参照できます。
pushState()の第1引数で指定したオブジェクトが、event.stateで取得できることに注目してください。
これにより、履歴エントリに任意のデータを紐付けておくことができます。
●popstateの発火条件
popstateイベントはブラウザの履歴操作に応じて発火しますが、どのようなアクションでpopstateが呼び出されるのか、もう少し詳しく見ていきましょう。
popstateイベントの発火条件を押さえておくことで、より柔軟なブラウザ履歴管理が実現できるはずです。
○history.pushState()とhistory.replaceState()
まず、JavaScriptのHistory APIを使って履歴エントリを追加・変更する場合を考えてみましょう。
history.pushState()は新しい履歴エントリをスタックに積む一方、history.replaceState()は現在の履歴エントリを置き換えます。
pushState()で追加された履歴エントリは、ブラウザの戻るボタンで戻った時にpopstateイベントを発火させます。
一方、replaceState()は履歴エントリを置き換えるだけなので、それ自体ではpopstateイベントを発火しません。
ただし、replaceState()で変更された履歴エントリも、後から戻るボタン等で参照された時にはpopstateイベントを発火します。
○ブラウザの戻る/進むボタン
次に、ユーザーがブラウザの戻る/進むボタンをクリックした場合について説明しましょう。
当然ながら、戻るボタンで一つ前の履歴に戻った場合には、popstateイベントが発生します。
進むボタンで次の履歴に進んだ場合も同様に、popstateイベントが発火されます。
ただし、現在のページで履歴スタックの最新の状態である場合、進むボタンは無効化されているはずです。
したがって、普通は最新の履歴状態でpopstateイベントが発生することはありません。
○サンプルコード2:pushStateとpopstate
pushState()とpopstateイベントの連携を確認してみましょう。
次のサンプルコードでは、pushState()で新しい履歴エントリを追加した後、戻るボタンで一つ前の履歴に戻ります。
// popstateイベントリスナーを登録
window.addEventListener('popstate', function(event) {
console.log('popstateイベントが発火しました');
console.log(event.state);
});
// 1秒後に履歴エントリを追加
setTimeout(function() {
history.pushState({ page: 1 }, '1ページ目', '?page=1');
console.log('履歴エントリを追加しました');
}, 1000);
// 3秒後にhistory.back()で戻る
setTimeout(function() {
history.back();
console.log('1つ前の履歴に戻りました');
}, 3000);
このコードを実行すると、次のようなログが出力されるはずです。
履歴エントリを追加しました
1つ前の履歴に戻りました
popstateイベントが発火しました
null
pushState()で履歴エントリを追加してから2秒後、history.back()で1つ前の履歴に戻ると、popstateイベントが発火していることが確認できます。
最初のページには履歴エントリが関連付けられていないため、event.stateはnullになっています。
○サンプルコード3:replaceStateとpopstate
同様に、replaceState()の動作も見てみましょう。
先ほどのコードのpushState()をreplaceState()に置き換えて実行してみます。
// popstateイベントリスナーを登録
window.addEventListener('popstate', function(event) {
console.log('popstateイベントが発火しました');
console.log(event.state);
});
// 1秒後に履歴エントリを置換
setTimeout(function() {
history.replaceState({ page: 1 }, '1ページ目', '?page=1');
console.log('履歴エントリを置換しました');
}, 1000);
// 3秒後にhistory.back()で戻る
setTimeout(function() {
history.back();
console.log('1つ前の履歴に戻りました');
}, 3000);
実行結果
履歴エントリを置換しました
1つ前の履歴に戻りました
popstateイベントが発火しました
null
replaceState()では履歴エントリが置換されるだけで新しいエントリは追加されないので、history.back()で戻った際にはreplaceState()で設定したstateオブジェクトは参照されません。
●popstateが発火しない場合
これまで、popstateイベントが発火するケースについて見てきましたが、逆にpopstateイベントが発火しない場合もあることを知っておく必要があります。
ここでは、代表的な2つのケースを取り上げましょう。
○ページ読み込み時のpopstate
1つ目は、ページを読み込んだ直後のpopstateイベントについてです。
直感的には、ページ読み込み時にもpopstateが発火しそうなものですが、実際にはそうではありません。
仕様上、ページ読み込み時のpopstateイベントは抑制されています。
ただし、ページ読み込み後に何らかの理由でhistory.back()やhistory.forward()が呼び出された場合には、popstateイベントが発火します。
つまり、ページ読み込み時だからと言ってpopstateイベントが絶対に発生しないわけではないのです。
popstateイベントリスナーの中で現在のURLをチェックし、ページ読み込み時の動作を制御する必要がある場合は、このことに留意しておきましょう。
location.hrefやlocation.pathnameなどを使ってURLを判定するのが一般的です。
○iframeでのpopstate
2つ目は、iframe内で発生したpopstateイベントについてです。
iframeの中で履歴をさかのぼっても、親ウィンドウ側ではpopstateイベントは発火しません。
あくまでもiframe内に閉じた動作となります。
複数のiframeを組み合わせて構成されるようなWebアプリケーションを開発する際は、iframe間の履歴同期についても検討が必要になるかもしれません。
親ウィンドウとiframe間でメッセージングを行うのが一般的な対処法です。
○サンプルコード4:ページ読み込み時のpopstate対策
それでは、ページ読み込み時のpopstateイベント発火を抑制するサンプルコードを書いてみましょう。
// popstateイベントリスナーを登録
window.addEventListener('popstate', function(event) {
// 初回アクセス時のURLを保持しておく
const initialURL = location.href;
return function() {
// 現在のURLを取得
const currentURL = location.href;
// 初回アクセス時のURLと比較
if (currentURL !== initialURL) {
// URLが変化している場合のみ、処理を実行
console.log('popstateイベントが発火しました');
console.log(event.state);
}
};
}());
このコードでは、即時実行関数を使って初回アクセス時のURLを保持しています。
popstateイベントが発生すると、現在のURLと初回アクセス時のURLを比較し、異なる場合のみ処理を実行するようにしています。
実行結果
// ページ読み込み直後
// (何も出力されない)
// ページ内リンクをクリック
popstateイベントが発火しました
null
// ブラウザバックで戻る
popstateイベントが発火しました
null
ページを読み込んだ直後はpopstateイベントが発火していないことが分かります。
一方、ページ内リンクをクリックして別のURLに遷移した後、ブラウザバックで戻ると、popstateイベントが正しく発火していますね。
●ブラウザバックの検知と制御
ここまでの内容を振り返ってみると、popstateイベントを使えばブラウザの履歴操作を検知できることがわかったかと思います
でも実際の開発現場では、ブラウザバックを検知するだけでなく、それをトリガーにして何らかの処理を実行したいというケースも多いのではないでしょうか。
例えば、フォームへの入力内容を保存していない状態でブラウザバックされると、せっかく入力したデータが失われてしまいます。
こうした事態を防ぐために、ブラウザバック時に確認ダイアログを表示するなどの施策が考えられます。
○ブラウザバック検知の方法
popstateイベントを使ったブラウザバック検知の基本的な実装パターンを見てみましょう。
ブラウザバックを検知する一般的な方法は、popstateイベントにリスナーを登録することです。
ブラウザの戻るボタンが押された時には必ずpopstateイベントが発火されるため、このイベントをハンドリングすることでブラウザバックを検知できます。
次のようなコードを記述することで、ブラウザバック時に特定の処理を実行できます。
window.addEventListener('popstate', function(event) {
// ブラウザバック時の処理
console.log('ブラウザバックが検知されました');
// 必要に応じてイベントのキャンセル
event.preventDefault();
// その他の処理...
});
ただし、popstateイベントはブラウザバック以外にも発火するケースがあるので、単純にこのイベントをハンドリングするだけでは不十分な場合があります。
○サンプルコード5:ブラウザバックの検知
それでは、ブラウザバックを検知して、アラートを表示するサンプルコードを書いてみましょう。
// 初回アクセス時のURLを保持しておく
history.replaceState(null, null, location.href);
window.addEventListener('popstate', function(event) {
// 現在のURLを取得
const currentURL = location.href;
// 初回アクセス時のURLから変化している場合のみ、アラートを表示
if (currentURL !== event.state) {
alert('ブラウザバックが検知されました');
}
});
実行結果
// ページ内リンクをクリック
(何も表示されない)
// ブラウザバックで戻る
(アラートが表示される)
ブラウザバックが検知されました
ページ内リンクをクリックしただけではアラートが表示されませんが、ブラウザバックで戻ると、アラートが表示されることが確認できました。
ここで、コードの1行目に注目してください。
history.replaceState()を使って、初回アクセス時のURLをstateオブジェクトに保存しています。
こうすることで、state情報の有無によってブラウザバックを判定できるようになります。
○ブラウザバックの禁止とリダイレクト
応用例として、ブラウザバックを禁止して、強制的に元のページにリダイレクトさせる方法を紹介しましょう。
ブラウザバックを禁止するには、popstateイベント発生時に history.pushState()を呼び出して、履歴スタックに新しいエントリを追加します。
これにより、ブラウザバックしても1つ前の画面には戻れなくなります。
代わりに、スクリプトで指定した任意のURLにリダイレクトさせることができます。
以下は、ブラウザバック時に強制的にトップページにリダイレクトするコード例です。
window.addEventListener('popstate', function(event) {
// ブラウザバック時にトップページへリダイレクト
history.pushState(null, null, '/');
window.location.href = '/';
});
ただし、ブラウザバックを完全に禁止することはユーザビリティの観点からおすすめできません。
本当に必要な場面以外では控えめに使うべき技法だと言えます。
○サンプルコード6:ブラウザバックの禁止
それでは、先ほどのサンプルコードを改変して、ブラウザバック時に強制的にリダイレクトさせてみましょう。
// 初回アクセス時のURLを保持しておく
const initialURL = location.href;
history.replaceState(null, null, initialURL);
window.addEventListener('popstate', function(event) {
const currentURL = location.href;
if (currentURL !== initialURL) {
// ブラウザバック時にinitialURLにリダイレクト
history.pushState(null, null, initialURL);
window.location.href = initialURL;
}
});
実行結果
// ページ内リンクをクリック
(画面が遷移する)
// ブラウザバックで戻ろうとする
(初回アクセスのURLに強制的にリダイレクトされる)
イベントリスナーの中で history.pushState() を呼び出すことで、ブラウザバック時の履歴エントリを上書きし、強制的にリダイレクトを実行しています。
●popstateのブラウザ対応状況
これまでpopstateイベントの基本的な使い方やユースケースについて見てきましたが、実際にこれらの機能を活用する前に、ブラウザ間の対応状況を確認しておく必要があります。
せっかく実装しても、特定のブラウザで動作しなければ意味がありませんからね。
○主要ブラウザでのサポート状況
popstateイベントは、HTML5の仕様として比較的早い段階から定義されていたため、現在の主要ブラウザではほぼ問題なく利用できます。
具体的には、Chrome、Firefox、Safari、Edgeの最新バージョンだけでなく、Internet Explorer 10以降でもサポートされています。
モバイル向けのブラウザも同様で、iOS safari、Chrome for Android、Firefox for Androidなどで広く利用可能です。
実際に、Can I use… などの対応状況まとめサイトで確認してみると、popstateイベントの対応率は95%以上とかなり高いことがわかります。
ですから、現代的なWebアプリケーション開発において、ほとんどの環境でpopstateを活用できると言えるでしょう。
○Safari10以前の問題と対応策
とはいえ、すべてのブラウザが完全に同じ挙動をするわけではありません。
特に、モバイルSafariには注意が必要です。
Safari 10より前のバージョンでは、ページ読み込み時にpopstateイベントが発火しないという問題があります。
これは、WebKitのバグに起因する動作で、他のブラウザとは異なる挙動となっています。
具体的には、Safari 10未満では初回ページアクセス時にpopstateイベントが発生せず、その後の画面遷移では期待通り発火するという状態です。
つまり、1つ前のページから戻ってきた場合に、初回アクセス時の状態を復元できないわけです。
アプリケーションの状態管理の実装によっては、データの不整合などが発生する可能性もあります。
この問題に対処するためには、ページ読み込み時とpopstateイベント発生時の両方で、画面の状態を復元する処理を記述する必要があります。
○サンプルコード7:Safariでのpopstate対応
それでは、Safari 10未満でもpopstateイベントを利用するサンプルコードを書いてみましょう。
location.hrefを使って、ページ読み込み時の処理を共通化するのがポイントです。
// ページ読み込み時とpopstateイベント発生時に呼び出される関数
function restoreState(event) {
// URLから状態を復元する処理
const currentURL = location.href;
console.log('状態を復元します。現在のURL:', currentURL);
// 必要に応じて、event.stateを利用して状態を復元
console.log('保存されていた状態:', event.state);
}
// ページ読み込み時にrestoreState()を呼び出す
window.addEventListener('load', function(event) {
restoreState(event);
});
// popstateイベント発生時にrestoreState()を呼び出す
window.addEventListener('popstate', function(event) {
restoreState(event);
});
実行結果
// ページ読み込み時
状態を復元します。現在のURL: https://example.com/
保存されていた状態: null
// 一覧ページに遷移
状態を復元します。現在のURL: https://example.com/list
保存されていた状態: null
// 詳細ページに遷移
状態を復元します。現在のURL: https://example.com/detail
保存されていた状態: null
// ブラウザバックで一覧ページに戻る
状態を復元します。現在のURL: https://example.com/list
保存されていた状態: null
Safari 10未満でも、ページ読み込み時とブラウザバック時の両方で状態が復元されていることが分かります。
実際のアプリケーション開発では、URLだけでなくevent.stateに保存した値を使って、よりリッチな状態管理が可能になるでしょう。
まとめ
今回は、JavaScriptのpopstateイベントについて、基本的な使い方からブラウザバック検知、Safari対応まで幅広く解説しました。
popstateを適切に活用することで、ユーザビリティの高いシングルページアプリケーションを開発できます。
ブラウザの履歴管理はSPAにおいて重要な要素ですが、popstateイベントを利用することでページ状態の復元や、戻る/進むボタンの動作制御が可能になります。
記事中のサンプルコードを参考に、pushState()やreplaceState()と組み合わせたpopstateの活用法を習得してください。
ただし、popstateの発火しないケースや、ブラウザごとの挙動の違いには注意が必要です。
ユーザ体験を損なわないよう、ブラウザバックの制御は慎重に行いましょう。
popstateを賢く使いこなして、快適なWebアプリケーションを開発してください。