目次
はじめに
Webアプリケーションをつくる中で、「編集中の画面で戻るボタンや更新ボタンが押されたら、本当にページ移動して良いかを尋ねる確認ダイアログを出したい!」というリクエストは、特に業務アプリケーションだとまあまあ「あるある」かと思います。こんな出だしで始めるということは、そうです、かくいう私も最近そんなリクエストを受けたわけです。
戻るボタンや更新ボタンはWebブラウザの機能であってWebアプリケーションの機能ではないし、Webブラウザも色々な種類があってそれぞれ色々な動きをするし、正直なところ「Webブラウザの機能には手を出さないでおこうよ~」というのが心の声ではあります。が、リクエストがあるってことは需要があるってことですしね!実現手段を考えてみることにしました。
手段自体はググればすぐ見つかるのですが、実際に動かしてみないとわからないこともある!ということで、今回の記事では、私なりに色々検証してみた結果を共有したいと思います。
今回の検証ポイント
今回の検証ポイントは以下の2点です。
- ボタン(戻るボタン、更新ボタン)が押されたことを検知できるか?
- (検知できた場合、)独自のダイアログを表示できるか?
検証ポイント2の「独自のダイアログ」は、ブラウザ標準のダイアログ(JavaScriptのalertやconfirmで出せるやつ)ではなく、自分でつくったダイアログ、という意味です。具体的なイメージを共有すると、以下の2つのダイアログのうち、上がブラウザ標準のダイアログで、下が独自のダイアログです。デザインにこだわりたいときなんかは独自でつくりたいですよね。
検証ポイント1:ボタン押下の検知
検知できる or できないについて、最初に結論を言ってしまうと「できる」です。
これはググれば一発なのですが、window.onbeforeunload(windowのbeforeunloadイベント)で検知できます。このイベントは、今表示されているページを閉じる直前に発生するイベントです。
「閉じる」という表現には、「ブラウザ(またはタブ)を閉じる」以外に「(自ページに、または他ページに)画面遷移する」も含まれています。今回は戻るボタンと更新ボタンの押下を検知したいわけですが、どちらもここでは画面遷移に当たります。
ちなみにソースコード(JavaScript、jQuery使用)はこんな感じです。
1 2 3 |
$(window).on("beforeunload", function(e) { // アンロードされたときの処理 }); |
どんなボタンが押されたかはわかるのか?
beforeunloadで検知できる幅が広いので、今回みたいに戻るボタンと更新ボタンが押されたときだけダイアログを表示したい場合は、beforeunloadイベントの発生源を知る必要があります。
これもググれば一発なのですが、PerformanceNavigationTimingインターフェースというものがあって、その中のtypeプロパティを調べたらいいよ、とのことです。
ちなみに、似たようなものでPerformanceNavigationインターフェースもあるのですが、こちらは今非推奨になっています。とはいえ、PerformanceNavigationTimingもまだ実験的な機能だそうな…注意して使えってことですかね。
typeプロパティにはどんな値が入っているのか?についてですが、まとめると以下の4つです。
値 | 概要 |
navigate | リンクのクリック、ブラウザのアドレスバーへのURL入力、フォーム送信、その他色々(reloadとback_forwardに書かれていること以外) |
reload | ブラウザのリロード、またはlocation.reload() |
back_forward | ブラウザの履歴操作(戻るボタン、進むボタン) |
prerender | リソースヒントのprerender |
戻るボタンはback_forward、更新ボタンはreloadかな?と期待できます。
試しに実装してみるとこんな感じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$(window).on("beforeunload", function(e) { let navigationType = window?.performance?.getEntriesByType("navigation")[0].type; // let navigationType = e.currentTarget.performance.navigation.type; // この書き方は非推奨 switch(navigationType) { case "navigate": console.log(navigationType + " : ページ遷移"); break; case "reload": console.log(navigationType + " : ページ更新"); break; case "back_forward": console.log(navigationType + " : 戻る・進む"); break; case "prerender": console.log(navigationType + " : prerender"); break; } }); |
すごく簡易的ですが、ブラウザのコンソールにログが出るようにしてみました。テキトーに画面も作ってみました(以下)。
テキトーさがわかりやすくにじみ出ちゃってますがまあいいでしょう。SampleページとSampleページ2を行き来できるリンクも設置してみました。JavaScriptの処理はどちらの画面も同じ(先述のコード)です。prerenderは発生頻度が低そうだったので今回はちょっと実装は省略です。
Sampleページをブラウザで開き、「Sampleページ2へ」リンクをクリックしてみると…
1 |
navigate : ページ遷移 |
ログ出た~!期待どおり!
では、色々なパターンを試してみます。操作とtypeの期待値、そして実際に操作してみた結果をまとめたものが以下になります。
No. | 操作 | type(期待値) | type(結果) |
1 | Sampleページをブラウザで開く | (初期表示なのでbeforeunloadは発生しない) | (初期表示なのでbeforeunloadは発生しない) |
2 | 「Sampleページ2へ」リンクをクリック | navigate | navigate |
3 | 「Sampleページへ」リンクをクリック | navigate | navigate |
4 | 更新ボタンをクリック | reload | navigate |
5 | 戻るボタンをクリック | back_forward | reload |
6 | 「Sampleページへ」リンクをクリック | navigate | back_forward |
え?期待値と結果が絶妙にズレている…???
何度やり直してみてもやっぱり1つズレます。もしかしたら実装に問題があるのかもしれないのですが、何が間違っているのかもわからない…。
今のところ、この検証から得た私見としては、「typeを取得できる仕組みはあるけど、これ頼みの実装をしてはいけない(かもしれない)」ということですね。
逆の発想で考える
今一度やりたいことを整理すると、「戻るボタンと更新ボタンが押されたときだけダイアログを表示したい」。逆に言えば、「戻るボタンと更新ボタンが押されたとき以外はダイアログを表示したくない」なので、こちらのアプローチで実装してみます。
実は、特定の場合にbeforeunloadイベントを適用外にできるのです!実装してみるとこんな感じです。
1 2 3 |
$("a").on("click", function() { $(window).off("beforeunload"); }); |
beforeunloadをoffしたらいいんですね。この例は、リンクをクリックした場合にbeforeunloadをoffにする、という実装です。
私の場合はjQueryを使ってon()関数でbeforeunloadイベントを登録したので、offするときも上記のようにoff()関数が使えます。jQueryを使わずにwindow.onbeforeunloadでイベント登録した場合は、以下のようにイベント処理をnullで書き換えると良いみたいですね。
1 |
window.onbeforeunload = null; |
リンクをクリックしたときにbeforeunloadをoffする実装を追加して、確かにリンククリック時のログは出力されないことが確認できました!beforeunload適用外のイベントが多いほどoffの処理を書かなくてはならないのが面倒ではありますが、今のところ、type判定よりも確実な方法に思えます。
ちなみに、戻るボタンは別のやり方でも検知できる
実は、戻るボタンと進むボタンはwindow.onpopstateでも検知できます。popstateイベントは、History APIと連動するイベントです。
1 2 3 |
$(window).on("popstate", function(e) { alert("戻るよ"); }); |
popstateイベントは、history.pushState()で生成した履歴に対してhistory.back()やhistory.foward()をすることで呼び出されるイベントです。そのため、popstateイベントに対して処理を定義しても、history.pushState()しなければそもそもイベント自体発生しません。pushStateするものがないよー!という方は、以下の1行を書けば一応popstateが動くようにはなります。
1 |
history.pushState({}, "", new URL(window.location)); |
たぶんパラメータは何でもいいような気がするんですが、そもそもが不要なpushStateって感じがしてあんまり気持ちよくはないですよねえ…良い案求む…。
ちなみに、popstateイベントとbeforeunloadイベントをどちらも登録した場合、戻る・進むボタンを押すと、popstateイベントが反応します。
検証ポイント2:独自ダイアログの表示
検証ポイント1が思った以上に長くなってしまいましたが、もうひとつの検証ポイントに進みます。
戻るボタン、更新ボタンが押されたことを検知できる場合、独自のダイアログを表示できるか?ですが、私的結論は、「popstateならできる、beforeunloadはできない」です。それぞれのパターンについて、詳細を見てみます。
popstateイベントで検知する場合
これはもう先に話したとおり、popstateイベントで検知さえできれば、それ以降は自由に独自のダイアログを表示できます。
1 2 3 |
$(window).on("popstate", function(e) { // 独自のダイアログを表示する処理 }); |
beforeunloadイベントで検知する場合
もしかすると検証ポイント1の時点で気づいた方もいるかもしれないですが、beforeunloadイベントのドキュメントを読むと何か色々と書いてあります。(ざっと以下)
- beforeunloadイベントによって、ウェブページがダイアログボックスを表示し、ユーザーにページを終了するかどうかの確認ができる
- 確認ダイアログを表示するには、event.preventDefault()を呼び出すこと
- しかし、全てのブラウザがevent.preventDefault()に対応しているわけではないため、event.returnValueプロパティに文字列を設定する、または文字列を返却すること
つまり、勝手にダイアログを表示してくれる仕組みが既にあるということです。ドキュメントのとおりに実装すると以下のように書けます。
1 2 3 4 5 |
$(window).on("beforeunload", function(e) { e.preventDefault(); // e.returnValue = "本当にページを終了しますか?"; return "本当にページを終了しますか?"; }); |
これを実行すると、beforeunloadイベントが呼び出される度に以下のダイアログが表示されるようになります。
ドキュメントにも書いてありますが、設定した文字列がダイアログに表示されるとは限らないですね。というか、今はほとんどのブラウザが独自のメッセージは設定できない仕様のようなので、もはや設定値は空文字でもいいくらいです。ちなみに、Chromeの場合は文字列を返却しないとうまく動作しませんでした。
さて、ダイアログ表示できるのはわかったのですが、これは独自のダイアログではなくてブラウザ標準のダイアログですね…。ということで、独自のダイアログを表示しようと色々やってみたのですが、結論、無理でした。
preventDefaultと文字列返却をしなければ標準のダイアログは表示されませんが、独自のダイアログを表示する処理を書いてみても、期待する動きにはなりません。私の場合、ページを離れる直前に一瞬独自ダイアログが描画されたのが目視確認できますが、有無を言わせずにページが終了します。
つまり、独自処理でページの終了を制御できる仕組みは用意されていないということですね。むしろ制御できてしまっては問題なので、これが正しい形だと思います。
まとめ
全体をまとめると、
- ページ終了(画面遷移含む)時の確認ダイアログは表示できる
- 独自のダイアログを表示できるかは場合による
ということでしょうか。(ちゃんとまとめられてる?)
今回の記事の内容はブラウザの仕様に依存する内容がほとんどですので、ブラウザの種類やバージョンによっては異なる挙動となる可能性があること、ご承知おきください!
執筆者プロフィール
- 社内の開発プロジェクトの技術支援や、新技術の検証に従事しています。主にアプリケーション開発系支援担当で、Java&サーバサイドが得意です。最近は、サーバーレスonAWSを推進しています。