ブラウザの「戻る」「進む」ボタンが押されたことを検知する

はじめに
ブラウザの「戻る」「進む」ボタンでページ遷移したことを検知するにはどうしたらよいでしょうか?
popstate
イベント 1 を使用する方法がありますが、popstate
は 通常のページ遷移(別ページに移動) では発生しません。同一ページ内で pushState
による履歴管理をしている場合のみ意味があります。別ページに遷移した時点で直前のページは破棄されますので、popstate
を含む全てのイベントハンドラも破棄されます。また、「戻る」「進む」のどちらなのかを判定する情報は提供されません。
performance.navigation.type
2 を使用する方法もあります。こちらは別ページへの遷移で使用できますが、HTML のキャッシュが無効の場合、type の値は常に 1: TYPE_RELOAD
になってしまい、通常のページ遷移と区別することができません。キャッシュが有効な場合でも「戻る」「進む」の時の type の値は 2: TYPE_BACK_FORWARD
ですので、popstate
と同様に「戻る」「進む」のどちらなのかを判定する情報は提供されません。また、performance.navigation.type
は非推奨の機能になったため、今後はブラウザが対応しなくなる可能性があります。
ここでは sessionStorage
と history.state
3 を併用した方法をご紹介します。
各方法の違いは以下となります。
方法 | 戻る/進む判定 | キャッシュ無効時 | 備考 |
---|---|---|---|
popstate |
〇 | × 4 | SPA 向け |
performance.navigation.type |
〇(古い仕様) | ×(常に reload) | 廃止予定 |
sessionStorage + history.state |
◎ | ◎ | 通常のページ遷移向け |
コード
- 通常のページ遷移
- 戻るボタンでの遷移
- 進むボタンでの遷移
- どれでもない
これらを判定するコードは以下です。
window.addEventListener('pageshow', function () {
// sessionStorage から遷移前のタイムスタンプを取得
const prevTimestamp = Number(sessionStorage.getItem('prevTimestamp') ?? -1);
// history.state に保存したタイムスタンプを取得
let currentTimestamp = history.state?.timestamp ?? null;
// 新しいページ遷移かどうかを判定
if (!currentTimestamp) {
console.log('新しいページ遷移');
currentTimestamp = Date.now();
if (currentTimestamp === prevTimestamp) {
currentTimestamp += 1; // 同じタイムスタンプになることは無いと思うが念のため
}
// history.state にタイムスタンプを保存
history.replaceState({
timestamp: currentTimestamp
}, '');
} else {
console.log('履歴内の移動');
if (prevTimestamp > currentTimestamp) {
console.log('戻るボタン');
} else if (prevTimestamp < currentTimestamp) {
console.log('進むボタン');
} else {
console.log('どれでもない');
}
}
sessionStorage.setItem('prevTimestamp', currentTimestamp);
});
解説
たとえば、以下のようにページ遷移した場合を例にします。
履歴の順序 | タイムスタンプ | ページ |
---|---|---|
1 | 15:00 | page1 |
2 | 15:10 | page2 |
3 | 15:20 | page3 |
4 * | 15:30 | page1 |
*
は現在表示しているページです。
新しいページ遷移のたびに history.sate
にタイムスタンプを設定していくので、以下のようになっています。
履歴の順序 | history.state.timestamp | ページ |
---|---|---|
1 | 15:00 | page1 |
2 | 15:10 | page2 |
3 | 15:20 | page3 |
4 * | 15:30 | page1 |
Session Storage に保存している prevTimestamp
は 15:30 です。
戻る
ここで「戻る」ボタンで遷移すると、3 番目のページに移動して、history.state.timestamp
は 15:20 です。
prevTimestamp(15:30)
> history.state.timestamp(15:20)
なので「戻る」だとわかります。
履歴の順序 | history.state.timestamp | ページ |
---|---|---|
1 | 15:00 | page1 |
2 | 15:10 | page2 |
3 * | 15:20 | page3 |
4 | 15:30 | page1 |
prevTimestamp
は 15:20 に更新されます。
進む
次に「進む」ボタンで遷移すると、4 番目のページに移動して、history.state.timestamp
は 15:30 です。
prevTimestamp(15:20)
< history.state.timestamp(15:30)
なので「進む」だとわかります。
履歴の順序 | history.state.timestamp | ページ |
---|---|---|
1 | 15:00 | page1 |
2 | 15:10 | page2 |
3 | 15:20 | page3 |
4 * | 15:30 | page1 |
prevTimestamp
は 15:30 に更新されます。
新しいページ遷移
今度は「戻る」「進む」ではなく通常のページ遷移を行ってみます。ページ内のリンクや、ブラウザの URL 欄に直接入力して、page3 を表示したとします。
この時、history には新しいエントリが追加されます。
新しいエントリですので、history.state
には何も入っていません。
そのため「新しいページ遷移」とわかります。
次の判定に備えて現在のタイムスタンプを history.state
に設定します。
履歴の順序 | history.state.timestamp | ページ |
---|---|---|
1 | 15:00 | page1 |
2 | 15:10 | page2 |
3 | 15:20 | page3 |
4 | 15:30 | page1 |
5 * | 15:40 | page3 |
prevTimestamp
は 15:40 に更新されます。
どれでもない
再度、通常のページ遷移を行ってみます。先ほどと同様に page3 に通常のページ遷移を行います。
あるいはリロードでも同様の動きになります。
この時、history に新しいエントリは追加されません。
history.state.timestamp
には 15:40
が設定されています。
prevTimestamp(15:40)
= history.state.timestamp(15:40)
これが「どれでもない」のパターンです。 履歴の状態は変化しません。
履歴の順序 | history.state.timestamp | ページ |
---|---|---|
1 | 15:00 | page1 |
2 | 15:10 | page2 |
3 | 15:20 | page3 |
4 | 15:30 | page1 |
5 * | 15:40 | page3 |
prevTimestamp
は 15:40 のままになります。
ブラウザの履歴には、現在表示しているページの URL と同じ URL のページを表示した場合、履歴は増えない という仕様があるようです。履歴の先頭に限らず、page1 や page2 を表示しているタイミングで同じページを再表示しても履歴は増えません。
「どれでもない」と表現しましたが、「同じページに遷移した」という表現のほうが適切かもしれません。
補足
pageshow
イベント 5
キャッシュからページを復元した場合、load
イベントは発生しないのでpageshow
を使用しています。
history.state
コード例ではhistory.replaceState
でstate
を上書きしていますが、別の用途でstate
を使用している場合は、相互に上書きしないように考慮が必要です。sessionStorage
sessionStorage は同一オリジン・同一タブでのみ共有されるデータなのでオリジンをまたがる場合の判定には使用できません。
脚注
-
キャッシュが無効だと「戻る」「進む」ボタンで遷移したタイミングでリロードするため、
popstate
ハンドラは破棄されます。↩