2021.06.23
STAFF BLOG
スタッフブログ
TECHNICAL
テクログ
どうも!
最近、「地獄のブラウザバック友達」ができたわいです!
親にはあの子たちとは付き合うなと言われていたのですが、、はい。
ブラウザバックを検知して発火するpopstateイベント
まず、今回実装しようとした内容は、
一覧ページがあって、そこで詳細ボタンをクリックすると、詳細ページがモーダルとして表示される
そして、そのモーダルには一覧ページに戻るための閉じるボタンがあり、
ブラウザバックボタンを押下した場合も遷移せず、閉じるボタンと同じ挙動をする
というものです。
上記の挙動は、History API を使用して実装しました。(簡潔に書くためにjQueryを使ってます。すみません)
// 詳細ボタンを押下
$(document).on("click", ".js-detail-open", function() {
history.pushState(null, null, "{$詳細ページのurl}"); // ブラウザバック検知用に履歴を追加
modalOpen(); // モーダルを開く処理
});
// モーダル閉じるボタンを押下
$(document).on("click", ".js-detail-close", function() {
// popstateイベントを発火させる(ブラウザバック押下時と同じ挙動に)
history.back();
});
// ブラウザバック押下時にページ遷移せず、動的に追加したStateが履歴から取り除かれたときに発火
window.addEventListener("popstate", function() {
modalClose(); // モーダルを閉じる処理
});
簡単に書きましたが、モーダル閉じるボタン押下時に「history.back();」させて履歴を取り除いておくとか、下記bfcacheに出くわしてからいろいろ改善しました。
ポイントは、JSで動的に追加した履歴でないと、ブラウザバック時にpopstateイベントが発火しないというところです。
挙動についてのイメージは、下記記事が参考になるかと思います。
イメージさえ掴めれば、そこまでややこしい話ではないと思います。
JSも含めてすべてのページ情報をメモリにキャッシュするbfcache
bfcacheはWebページを速く表示させるためのブラウザの機能です。
詳細については、下記記事が参考になりました。
簡単に言うと見出しの通りで、具体的にどのようなことが起こるかというと、
ハンバーガーメニューを開いてリンクをクリックして、遷移した先でブラウザバックをすると、
ハンバーガーメニューが開いたままで先ほどのページが表示される
とかです。
以前ロードしたJSがそのまま使われて、ブラウザバック時にJSをロードしてくれない。
ページを離れたときの状態がそのまま再現されるというものです。
今回、このbfcacheが iOS の Safari / Chrome の両方で発生しました。
ブラウザバック時にキャッシュされているpopstateイベントが発火してバグる
bfcacheのせいで、ハンバーガーメニューが開いているだけならまだよかったのですが、
初回アクセス時に設定したpopstateイベントがブラウザバック時にそのまま発火して、
モーダルを閉じる処理(省略してますが、結構複雑…)を実行してしまうバグにハマりました。
bfcacheをJSで検知するには、PageTransitionEvent.persisted を使えば良いそうです。
ということで、下記記事を参考にして、
ページ読み込みがbfcacheからであれば、JSで強制リロードしてもう一度JSをロードするという処理を実装しました。
これで一件落着かと思えば、、、悲劇が起きました、、
連続で該当処理が入っているページへ遷移していき、その後ブラウザバックを連続で押していくと、
ブラウザバック2回まではページのリロードという期待通りの動きをしてくたのですが、
3回目のブラウザバック時に、JSでbfcacheを検知できない(bfcache自体が発動していない?)にもかかわらず、
popstateイベントが発火してしまいました。
ブラウザの挙動を見る感じ、履歴をpushStateで余分に追加した分のpopstateイベントが発火していました。
つまり、適切に履歴を取り除いていかないとこの問題は解決できないということです。
そこらへんはブラウザがよきにはからってくれると思っていましたが、そんなに甘くありませんでした。
最終的には、下記のような実装で、無事「地獄のブラウザバック友達」と仲良くなることができました。
let isBFCache = false;
window.onpageshow = function(event) {
// bfcacheを検知、popstateイベントより先に発火
isBFCache = event.persisted;
};
// 詳細ボタンを押下
$(document).on("click", ".js-detail-open", function() {
history.pushState(null, null, "{$詳細ページのurl}"); // ブラウザバック検知用に履歴を追加
modalOpen(); // モーダルを開く処理
});
// モーダル閉じるボタンを押下
$(document).on("click", ".js-detail-close", function() {
// popstateイベントを発火させる(ブラウザバック押下時と同じ挙動に)
history.back();
});
// ブラウザバック押下時にページ遷移せず、動的に追加したStateが履歴から取り除かれたときに発火
window.addEventListener("popstate", function() {
if (isBFCache) {
isBFCache = false;
return false;
}
modalClose(); // モーダルを閉じる処理
});
いざ該当箇所のコードだけ抜き出してみると考えやすいですが、
ソースコードの方は複雑すぎて、なかなか思考の整理ができませんでした。
思考が整理できさえすれば、あとはやることが決まってくるので、
いかに論理的に問題を分割できるかということが重要なスキルだなと改めて感じました。
まあ、そんな大層なことは言わなくていいんですよ。
友達が増えたので、それでOKです。
以上、わいでした。
健闘を祈る!!