SPAのPWAでAndroidの戻るボタン問題はこれで解決!

「PWAを作ったけどAndroidの戻るボタンを押すとアプリが終了するのを何とかしたい」
「シングルページアプリケーション(SPA)なので、アプリ独自のページ遷移があるけど、Androidの戻るボタンに対応したい」

こんにちは、最近PWA(Progressive Web Application)開発を楽しんでいるタカフです。

SPAでPWA開発をしていると必ずぶち当たる問題。

それは、Androidの戻るボタンを押すとアプリが終了してしまう問題ですね。

iOSはハードウェアとしての戻るボタンがそもそもないので、1つのHTMLページで作るいわゆるSPAならば、ソフトウェアで戻るも実装できるので問題ないのですが(例えばスワイプバック)、

Androidはハードウェア「戻るボタン」があるので、戻るボタンを押すとデフォルトの挙動としてはアプリ終了してしまうんですね。

これが、単純な単一ページ遷移アプリならHistoryAPIを使ってスタック的にpushStateをすればいいのですが、意外と制御が難しいのがタブ型アプリです。

一般的なタブ型アプリの「戻るボタン」の制御の例では、

タブAで2ページ目に進んだ後にタブBに切り替えて2ページ目に進んで、
そこで戻るボタンを押し続けていくと、
タブBの1ページ目→タブAの2ページ目→タブAの1ページ目→アプリ終了

となるのが最近の違和感ない動きなのかなと思います。

大手アプリであるTwitter・Facebook等を見ててもそんな感じです。

AndroidのネイティブアプリならActivity単位で移動しているので比較的簡単に戻る制御が出来ますが、
これがAndroidのPWAだと現在のアプリの状態に応じてどこに戻るかjs側で制御する必要があります

この記事ではそれをどうやって解決するかの内容となっています。もちろん単一ページ遷移アプリでも使えます。

結論:popstateイベントでアプリ状態に応じて戻る制御した後pushStateする

この問題の解決法の結論としては、HISTORY APIのpopStateイベントとpushSatateメソッドの間で戻る処理をjsで制御していく方法です。

具体的には、

AndroidのSPAのPWAではpopstateにイベント登録して戻るイベントをフックしておいて、
戻るボタン押下でイベント発火したらアプリ状態に応じてページ戻りを処理して
まだ戻れそうなページがある場合は、すぐにpushStateしておきます。

そうすれば、またpopstateで戻る処理を判断出来るようなります。

さらに、何かページ遷移した等の次に戻る制御が必要になった時にもpushStateするイベントを入れておきます。

これはもうコードを見た方が早いですね。次のようになります。

サンプルコード

このコード例は僕の推しUIフレームワークであるFramework7を使ったサンプルコードとなりますが、この考え方は他のSPAでも適用出来ます。

// これはAndroidのみの制御とする
if (app.device.android) {
  controlBackBtn();
}

// Backボタンの制御関数
function controlBackBtn() {
  // Homeタブの時で、ページ遷移があったイベントで戻るボタン制御
  $$('#view-home')[0].f7View.router.on('routeChange', function (newRoute, previousRoute, router) {
    console.log("view-home routeChanged");
    hookHistoryBack();
  });

  // Homeタブ以外で、タブが切り替わったイベントで戻るボタン制御
  $$('#app > .tabs > .tab').on('tab:show', function (e) {
    console.log("tabChanged");
    hookHistoryBack();
  });

  // HistoryAPIで履歴を追加して戻るコマンドを1回無効化します
  function hookHistoryBack() {
    if (!window.history.state || !window.history.state.hookBackBtn) {
      console.log("hook back btn.");
      window.history.pushState({hookBackBtn: true}, '')
    } else {
      console.log("already hooked back button.");
    }
  }

  // HistoryAPIのpopstateイベント
  window.addEventListener('popstate', function (event) {

    // 現在のタブの判断
    var activeTabId = $$('#app > .tabs > .tab-active').attr('id');
    // 現在のタブがHomeタブで、現在のページも1ページ目なら次の戻るボタンで終了となる
    if (activeTabId == "view-home" && $$('#view-home')[0].f7View.router.history.length === 1) {
      console.log("次のbackボタンで終了");
    } else {
      console.log("backボタン制御");
      // この時点でのアプリ戻るの処理をする
      controlAppBack();
      // この時点ではまだアプリ終了かを判断出来ないのでもう一度戻るボタン制御
      hookHistoryBack();
    }

  });

  // ここにそのアプリ内の戻るボタンの制御を記述する
  // タブ毎のjsに処理を委譲してもいいかもしれない。
  function controlAppBack() {
    // 現在のタブの判断
    var activeTabId = $$('#app > .tabs > .tab-active').attr('id');
    // Homeタブでも何でも2ページ目以降に進んでいるならページ戻す
    if ($$('#' + activeTabId)[0].f7View.router.history.length > 1) {
      $$('#' + activeTabId)[0].f7View.router.back();
    } else {
      // Homeタブに戻す
      app.tab.show("#view-home");
    }
  }
}

Homeタブからアプリ終了までには2回の戻るボタン押下が必要

すみません、上記のサンプルコードでは一つ欠点があります。

Homeタブから戻るボタンでアプリ終了するには2回押下する必要があります。

HISTORY APIにはpushStateメソッドはあってもそれを打ち消すメソッドはないんですね。なので一度pushStateメソッドで戻るボタン制御を入れてしまうと1回は戻るボタン押下が必要なので、このような欠点となります。

僕としてはSPAのPWAでも戻るボタンの制御を出来ることを天秤にかけたら許容範囲です。

余談

当初この問題に対してググって一番最初に見つかったページは以下のページなのですが、

https://stackoverflow.com/questions/43329654/android-back-button-on-a-progressive-web-application-closes-de-app

このベストアンサーだけでは以下の課題がありました。

  • 何故かload時のpushStateは画面上のどこかをタップしないと反映されない
    (つまりAndroidaとすぐアプリ終了する)
  • このサンプルコードだとずっと終了しない

ということで、stackoverflowのコードを工夫してタブ型アプリでも違和感なく戻る制御が出来るようにしてみました。

デモページ

デモページを用意しました。

以下のページをスマホで開いてホーム画面に追加してみてください。

https://kahoo.blog/data/demo/controll_back_btn/

Androidですと、Homeタブでページ遷移して「戻るボタン」を押すとページバックしたり、
タブ切り替えして「戻るボタン」押すとHomeタブにきちんと戻ります。

iOSの場合はスワイプバックでページバック出来ます。

以上、カフーブログの提供でお送りしました。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です