android html5 ionic

History APIを使ってIonic(3以前のSPA)でブラウザの戻るボタンやAndroidバックキーを押すと前サイトに戻ってしまう件に対応する

投稿日:2019年11月24日

Ionic2や3ではまだAngular Routerを採用していなかったので、ページ遷移をしてもブラウザ履歴が積まれず、Androidのバックキーやブラウザの戻るボタンを押すとサイトに入ってくる前のページまで戻らされてしまいます。

遷移した「About」ページでAndroidバックキーを押したら「Home」画面に戻って欲しいのに、アプリ自体が終わってしまいます。

このままだと使っているユーザもビックリするやらゲンナリするやらなので、HTML5仕様実装の「History API」を使ってこの件を回避してみます。

History API、Ionic3云々が必要ない方は対処部分からご覧ください。

検証環境

  • Windows 10
  • Node.js 10.16.1
  • Ionic 3.9.5(Cordova無しのWebのみ)
  • Angular 5.2.1
  • Chrome 78

History API

Webアプリ(Javascript)からブラウザの戻るボタンやAndroidのバックキーを制御するのは難しいので、代替手段としてブラウザ履歴を制御出来るHistory APIを使います。

pushState()でブラウザ履歴をプログラマブルに追加出来、window.onpopstateイベントハンドラでブラウザ履歴がpopされるタイミングを制御出来るようになります。

window.onpopstate = function(event) {
  alert("location: " + document.location + ", state: " + JSON.stringify(event.state));
};

history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");
history.replaceState({page: 3}, "title 3", "?page=3");
history.back(); // alerts "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // alerts "location: http://example.com/example.html, state: null
history.go(2);  // alerts "location: http://example.com/example.html?page=3, state: {"page":3}

URLが変わらない旧タイプのSPAでも一度pushState()してやり、popstateイベントハンドラを実装することで、戻るボタンクリックを検知できるようになります。

Ionic3プロジェクトを作って準備する

2019年末時点でIonic4なので今時3のプロジェクトを新規作成することは無いですが、この事象の再現性の為に残します。

(既に3のプロジェクトが有る方は読み飛ばしてください)

ionicインストール

C:\src\ionic>npm install ionic -g
C:\Program Files\nodejs\ionic -> C:\Program Files\nodejs\node_modules\ionic\bin\ionic
+ ionic@5.4.6
added 240 packages from 155 contributor

ionic3プロジェクト作成

ionic startでプロジェクトを作る際、「–type=ionic-angular」オプションを付けることでionic3のプロジェクトになります。テンプレートは予め幾つかページが作られるtabsを使用。

C:\src\ionic>ionic start withHistoryAPI tabs --type=ionic-angular

件のIonic3、Angular5のプロジェクトが出来ました。

メニューを実装する

tabsテンプレートには画面上部のメニューが無いので実装しておきます。3ファイル弄ります。

app.html

メニューの見た目を実装。

<ion-menu [content]="content" side="left" id="mainMenu">
    <ion-header>
        <ion-toolbar>
            <ion-title>メニュー</ion-title>
        </ion-toolbar>
    </ion-header>

    <ion-content>
        <ion-list>
            <button
                menuClose
                ion-item
                *ngFor="let p of pages"
                (click)="openPage(p)"
            >
                {{p.title}}
            </button>
        </ion-list>
    </ion-content>
</ion-menu>

<ion-nav [root]="rootPage" #content></ion-nav>

app.component.ts

メニューの内容、切り替え処理を実装。

import { Component, ViewChild } from "@angular/core";
import {
    Platform,
    Nav  // 追加
} from "ionic-angular";

    (snip)

import { HomePage } from "../pages/home/home";
import { AboutPage } from "../pages/about/about";
import { ContactPage } from "../pages/contact/contact";

@Component({
    templateUrl: "app.html"
})
export class MyApp {

    @ViewChild(Nav) nav: Nav;

    // メニューに表示する内容
    pages = [
        { title: "Home", component: HomePage },
        { title: "About", component: AboutPage },
        { title: "Contact", component: ContactPage }
    ];
    //    rootPage: any = TabsPage;
    rootPage: any = HomePage;

    (snip)

    openPage(page) {
        this.nav.push(page.component);
    }
}

home.html

HOME画面にハンバーガーアイコンを置いてメニューが起動するように実装。

<!-- <ion-header>
  <ion-navbar>
    <ion-title>Home</ion-title>
  </ion-navbar>
</ion-header> -->

<ion-header>
    <ion-navbar color="dark">
        <button ion-button menuToggle>
            <ion-icon name="menu"></ion-icon>
        </button>
        <ion-title>HOME</ion-title>
    </ion-navbar>
</ion-header>

以上で冒頭のGIFアニメーションのアプリが出来ました。

ブラウザ履歴がポップされた時の制御を入れる

ようやく本題です。

実装仕様

ブラウザの戻るボタンや、Androidのバックキーが押された時、

  1. モーダルダイアログの表示中は何もしない
  2. メニューが開かれている時はメニューを閉じる
  3. 画面スタックが積まれている(Nav#push()されている)場合はpop()
  4. 画面スタックが無い == Home画面に居る時はアプリ終了するかユーザと対話
    4.1. 「いいえ」がクリックされた場合は何もしない
    4.2. 「はい」がクリックされた場合はアプリに入る前のページに戻る。

実装開始

app.component.tsに以下差分を追加で実装します。

import {
    Platform,
    Nav,
    MenuController,
    App,
    AlertController
} from "ionic-angular";

export class MyApp {

    (snip)

    constructor(
          :
        // 追加
        private menu: MenuController,
        private app: App,
        private alertCtrl: AlertController
    ) {

        (snip)

        // ブラウザ戻るボタン、Androidバックキー制御
        this.setupBackButtonBehavior();
    }

    // 追加。肝の部分。
    setupBackButtonBehavior() {

        // 最初に一つブラウザ履歴を積んでおく
        history.pushState(null, null, "");

        // 「戻る」操作でブラウザ履歴が減るのをハンドリング
        window.addEventListener("popstate", () => {

            // メニューが開いていた場合はメニューを閉じる挙動にする
            if (this.menu.isOpen()) {
                this.menu.close();
                // onpopstateが発生するようにブラウザ履歴を積み直す
                history.pushState(null, null, "");
                return;
            }

            // モーダルダイアログが表示されている時はなにもしない
            let isModalShowing = window.document.querySelector(
                ".loading-wrapper, .alert-wrapper, .modal-wrapper"
            );
            if (isModalShowing) {
                // onpopstateが発生するようにブラウザ履歴を積み直す
                history.pushState(null, null, "");
                return;
            }

            // 画面遷移(Nav#push()されている状態か調査)
            let navCtrl = this.app.getActiveNav();
            if (navCtrl.canGoBack()) {
                // 画面スタック有り、popする。
                navCtrl.pop();
                // onpopstateが発生するようにブラウザ履歴を積み直す
                history.pushState(null, null, "");
            } else {
                // 画面スタック無し、アプリを終了するかユーザとインタラクション
                let exitConfirmDialog = this.alertCtrl.create({
                    title: "確認",
                    message: "アプリを終了しますか?",
                    buttons: [
                        {
                            text: "いいえ",
                            role: "cancel",
                            handler: () => {
                                // onpopstateが発生するようにブラウザ履歴を積み直す
                                history.pushState(null, null, "");
                            }
                        },
                        {
                            text: "はい",
                            handler: () => {
                                // 疑似的に積んであるブラウザ履歴を削除。
                                // 遷移元ページに戻す。PWAの場合はアプリ終了。
                                history.back();
                            }
                        }
                    ]
                });
                exitConfirmDialog.present();
            }
        });
    }

実装完了です。

動作確認

ionic serveでHTTPサーバを起動してAndroidのChromeからから見てみます。

C:\src\ionic\withHistoryAPI> ionic serve --address=0.0.0.0 --port=8100

操作毎の戻る操作は全てAndroidのバックキーで行っています。

  • 画面スタックがある場合はpopして前画面に戻る
  • メニューが開いている時はメニューを閉じる
  • ホーム画面に居る時はユーザにアプリを終了するか問う

が実装できました。

まとめ

  • 古いSPAでもHistory APIを組み合わせれば厄介な「戻る」制御が簡単になる。
  • 早くIonic4へマイグレ出来ればこんなことしなくていい (^_^;

-android, html5, ionic
-,

執筆者:

関連記事

UserAgent判定JSライブラリ「UAParser.js」と「Platform.js」の比較

2019年時点で開発が継続しているUA判定JSライブラリから2つ選択して動作を確認しました。 目次1 Webリソースから比較1.1 github比較1.2 NPMリポジトリ比較2 実際に使用して比較2 …

ionic-cli 5からcordovaを使ったハイブリッドアプリプロジェクトの作成方法が変わった

久しぶりにハイブリッドアプリを作ろう、ついでにionicとcordovaも最新にしよう。と作業を開始したら今までの手順と違って若干困惑した話です。 プロジェクト新規作成時にcordovaを使うか聞かれ …

Chrome75に実装された「Web Share API Level 2」を使ってみた

(2019/06現在、個人的な感想としては実戦投入はまだ早い印象でした) Webアプリにシェア機能を付けたい時があります。OSネイティブAPIを呼べないWebアプリではsharer.jsやremote …

hidden.inとSoftEtherで無料のビデオ会議環境を構築する

自粛が続いて会えない人も多くなり、人との交流が恋しくなりますね。 仕事ではミーティングをオンライン化するニーズが増え、以下メジャーなパブリックサービスを使っている人も増えていると思います。 Skype …

Angular、React、Vueプロジェクトを新規作成した時に生成されるリソース規模比較

アンチウィルスのディスクフルスキャンが日に日に遅くなっていく・・・ 一年後には丸一日掛かっても終わらなくなり、仕事終わりにかけて行ったフルスキャンが翌日まだ頑張っている。 この原因、ちょこちょこ作って …

 

shingo.nakanishi
 

東京在勤、1977年生まれ、IT職歴2n年、生涯現役技術者を目指しています。健康第一。