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のバックキーが押された時、
- モーダルダイアログの表示中は何もしない
- メニューが開かれている時はメニューを閉じる
- 画面スタックが積まれている(Nav#push()されている)場合はpop()
- 画面スタックが無い == 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へマイグレ出来ればこんなことしなくていい (^_^;