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
-,

執筆者:

関連記事

Android、iPhoneが向いている方向をWeb地図上で表現してみる(leaflet.js使用)

前回Javascriptでコンパス機能を実装して、Android、iPhoneの背面が向いている方角が数値(0~360:0が北)で分かるようになりました。 One IT Thing  9 P …

IndexedDBにblob保存されたPDFファイルを外部アプリに頼らずにJavascriptで表示(後編)

目次1 はじめに2 ビューワ機能を提供してくれるnpmモジュールを選別する3 実装開始3.1 ng2-pdfjs-viewerを使えるようにする3.2 PDFビューワコンポーネントを作る3.3 Hom …

Stripe+Java+Payment Request APIでApple Pay、Google Payを使ったテストWeb決済をしてみる

自分で作ったサービスを運用してチャリンチャリンしたい・・・エンジニアならこんな夢、一度は見たことがあるんじゃないでしょうか。 夢を実現する為、以前Stripeのcheckout.jsを使ったテストWe …

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

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

PWAストアの実現性についての考察

(*)この記事内では「Google Play」、「App Store」、「Microsoft Store」で公開されているアプリを「ストアアプリ」と呼んでいます。 先日B2Bアプリはストアアプリにする …

 

shingo.nakanishi
 

東京在勤、1977年生まれ、IT職歴2n年、生涯技術者として楽しく生きることを目指しています。デスマに負けず健康第一。