web-dev-qa-db-ja.com

Androidプログレッシブウェブアプリケーションの[戻る]ボタンでアプリを閉じる

「純粋な」HTML5/Javascript(プログレッシブ)Webアプリケーションは、アプリが終了しないようにモバイルデバイスの戻るボタンをインターセプトできますか?

この質問は this one に似ていますが、PhoneGap/IonicまたはCordovaに依存せずにこのような動作を実現できるかどうかを知りたいです。

19
rmpestano

Android戻るボタンは、プログレッシブWebアプリのコンテキスト内から直接フックすることはできませんが、目的の結果を達成するために使用できる履歴APIが存在します。

まず、ユーザーが表示しているページのブラウザ履歴がない場合、戻るボタンを押すとすぐにアプリが閉じます。
アプリを最初に開いたときに以前の履歴状態を追加することで、これを防ぐことができます。

window.addEventListener('load', function() {
  window.history.pushState({}, '')
})

この関数のドキュメントは mdn にあります:

pushState()は3つのパラメータを取ります。状態オブジェクト、タイトル(現在は無視されます)、および(オプションで)URL [...]指定されていない場合は、ドキュメントの現在のURLに設定されます。

したがって、ユーザーは戻るボタンを2回押す必要があります。 1回押すと元の履歴状態に戻り、次のプレスでアプリが閉じます。


パート2は、ウィンドウの popstate イベントにフックすることです。このイベントは、ブラウザーがユーザーアクションを介して履歴内を前後に移動するたびに発生します(したがって、history.pushStateを呼び出すときではありません)。

同じドキュメントの2つの履歴エントリ間でアクティブな履歴エントリが変更されるたびに、popstateイベントがウィンドウに送出されます。

だから今私たちは持っています:

window.addEventListener('load', function() {
  window.history.pushState({}, '')
})

window.addEventListener('popstate', function() {
  window.history.pushState({}, '')
})

ページが読み込まれると、すぐに新しい履歴エントリが作成され、ユーザーが「戻る」を押して最初のエントリに戻るたびに、新しいエントリが再び追加されます!


もちろん、このソリューションは、ルーティングのない単一ページのアプリにとってはとてもシンプルです。ユーザーがナビゲートする場所と現在のURLを同期させるために、すでに履歴APIを使用しているアプリケーションに適合させる必要があります。

これを行うには、履歴の状態オブジェクトに識別子を追加します。これにより、popstateイベントの次の側面を利用できます。

アクティブ化された履歴エントリがhistory.pushState()への呼び出しによって作成された場合、[...] popstateイベントの状態プロパティには、履歴エントリの状態オブジェクトのコピーが含まれます。

popstateハンドラーの実行中に、back-button-closes-appの動作を防止するために使用している履歴エントリと、アプリ内のルーティングに使用される履歴エントリを区別し、予防的な履歴のみを再度プッシュすることができます。特にポップされた場合のエントリ:

window.addEventListener('load', function() {
  window.history.pushState({ noBackExitsApp: true }, '')
})

window.addEventListener('popstate', function(event) {
  if (event.state && event.state.noBackExitsApp) {
    window.history.pushState({ noBackExitsApp: true }, '')
  }
})

最後に観察された動作は、戻るボタンが押されたときに、プログレッシブウェブアプリのルーターの履歴に戻るか、アプリが開かれたときに表示された最初のページに留まることです。

26
alecdwm

@alecdwm、それは純粋な天才です!

Android(ChromeおよびSamsungブラウザー))で動作するだけでなく、デスクトップWebブラウザーでも動作します。Chrome、Firefox、 WindowsではEdgeを使用しているため、Macでも結果は同じになる可能性があります。IEはEewであるためテストしませんでした。ほとんどの場合、[戻る]ボタンのないiOSデバイス向けに設計している場合でも、 Android(およびWindows Mobile ... awww ...貧弱なWindows Mobile)の戻るボタンが処理され、PWAがネイティブアプリのように感じられるようにすることをお勧めします。

イベントリスナーをロードイベントにアタッチすることはできませんでした。そのため、私はだまして、すでに持っている既存のwindow.onload init関数にそれを追加しました。

標準のWebページとしてP​​WAを閲覧しているときにPWAに移動する前に、実際に表示していたWebページに実際に戻りたいユーザーをイライラさせる可能性があることに注意してください。その場合は、カウンターを追加できます。ユーザーが2度ヒットした場合は、実際に「通常の」バックイベントの発生を許可できます(またはアプリを終了できます)。

Chrome on Androidまた、(何らかの理由で)空の履歴状態が追加されたため、実際に戻るにはさらに1つのBackが必要でした。誰かがそれについて洞察を持っているなら、私は興味があります理由を知っています。

これが私のフラストレーション防止コードです:

var backPresses = 0;
var isAndroid = navigator.userAgent.toLowerCase().indexOf("Android") > -1;
var maxBackPresses = 2;
function handleBackButton(init) {
    if (init !== true)
        backPresses++;
    if ((!isAndroid && backPresses >= maxBackPresses) ||
    (isAndroid && backPresses >= maxBackPresses - 1)) {
        window.history.back();
    else
        window.history.pushState({}, '');
}
function setupWindowHistoryTricks() {
    handleBackButton(true);
    window.addEventListener('popstate', handleBackButton);
}
6
John T.

このアプローチには、既存の回答に比べていくつかの改善点があります。

ユーザーが2秒以内に2回押した場合に終了することをユーザーに許可します:最適な期間は議論の余地がありますが、オーバーライドオプションを許可するという考えは、Androidアプリで一般的であるため、多くの場合、正しいアプローチです。

スタンドアロン(PWA)モードでのみこの動作を有効にします:これにより、Android Webブラウザー内でユーザーが期待するとおりにWebサイトが動作し続け、この回避策のみが適用されますユーザーがWebサイトを「実際のアプリ」として表示した場合。

function isStandalone () {
    return !!navigator.standalone || window.matchMedia('(display-mode: standalone)').matches;
}

// Depends on bowser but wouldn't be hard to use a
// different approach to identifying that we're running on Android
function exitsOnBack () {
    return isStandalone() && browserInfo.os.name === 'Android';
}

// Everything below has to run at page start, probably onLoad

if (exitsOnBack()) handleBackEvents();

function handleBackEvents() {
    window.history.pushState({}, '');

    window.addEventListener('popstate', () => {
        //TODO: Optionally show a "Press back again to exit" tooltip
        setTimeout(() => {
            window.history.pushState({}, '');
            //TODO: Optionally hide tooltip
        }, 2000);
    });
}
0
Luckyrat

反応アプリの内部でこれを処理するためにネイティブのJavaScript関数を使用したくなかったので、_react-router_または_react-dom-router_を使用するソリューションを精査しましたが、最終的には、期限までにネイティブのjsがうまくいきました。 componentDidMount()内に次のリスナーを追加し、履歴を空の状態に設定しました

_window.addEventListener('load', function() {
  window.history.pushState({}, '')
})

window.addEventListener('popstate', function() {
  window.history.pushState({}, '')
})
_

これはブラウザーでは問題なく機能しましたが、モバイルのPWAではまだ機能していませんでした。ようやく、同僚がコードを介して履歴アクションをトリガーすることがリスナーと出来上がりの原因であることがわかりました。すべてが整った

_window.history.pushState(null, null, window.location.href); 
window.history.back(); 
window.history.forward(); 
_
0
Paulo