Кнопка Android Back на прогрессивном веб-приложении закрывает приложение

Может ли "чистый" HTML5/Javascript (прогрессивное) веб-приложение перехватить кнопку возврата мобильного устройства, чтобы избежать выхода приложения?

Этот вопрос похож на этот, но я хочу знать, можно ли достичь такого поведения, не завися от PhoneGap/Ionic или Cordova.

Ответ 1

В то время как кнопка обратной связи Android не может быть напрямую подключена к контексту прогрессивного веб-приложения, существует история api, которую мы можем использовать для достижения желаемого результата.

Прежде всего, когда нет истории браузера для страницы, на которой пользователь включен, нажатие кнопки "Назад" немедленно закрывает приложение.
Мы можем предотвратить это, добавив предыдущее состояние истории, когда приложение открывается первым:

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

Документацию для этой функции можно найти на mdn:

pushState() принимает три параметра: объект состояния, заголовок (который в настоящее время игнорируется) и (необязательно) URL [...], если он не указан, он устанавливает текущий URL-адрес документа.

Поэтому теперь пользователю нужно дважды нажать кнопку "Назад". Одно нажатие возвращает нас к исходному состоянию истории, следующий пресс закрывает приложение.


Во второй части мы подключаемся к событию popstate окна, которое запускается всякий раз, когда браузер перемещается назад или вперед в истории с помощью действия пользователя (поэтому не при вызове history.pushState).

Событие popstate отправляется в окно каждый раз, когда активная запись истории изменяется между двумя записями истории для того же документа.

Итак, теперь у нас есть:

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

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

Когда страница загружается, мы немедленно создаем новую запись истории, и каждый раз, когда пользователь нажимает "назад", чтобы перейти к первой записи, мы снова добавляем новую запись!


Конечно, это решение настолько просто для одностраничных приложений без маршрутизации. Он должен быть адаптирован для приложений, которые уже используют историю api, чтобы поддерживать текущий url в синхронизации с тем, где пользователь переходит.

Для этого мы добавим идентификатор объекта состояния истории. Это позволит нам воспользоваться следующим аспектом 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 }, '')
  }
})

Последнее наблюдаемое поведение заключается в том, что при нажатии кнопки "Назад" мы либо возвращаемся в историю нашего прогрессивного маршрутизатора веб-приложений, либо остаемся на первой странице, видимой при открытии приложения.

Ответ 2

@alecdwm, это чистый гений!

Он работает не только на Android (в Chrome и браузере Samsung), но и работает в настольных веб-браузерах. Я тестировал его на Chrome, Firefox и Edge в Windows, и, скорее всего, результаты будут одинаковыми на Mac. Я не тестировал IE, потому что раньше. Даже если вы в основном разрабатываете устройства iOS, у которых нет кнопки "назад", по-прежнему стоит подумать, что обратные кнопки Android (и Windows Mobile... awww... poor Windows Mobile) обрабатываются так, чтобы PWA чувствовал себя больше похоже на родное приложение.

Прикрепление прослушивателя событий к событию загрузки не сработало для меня, поэтому я просто обманул и добавил его к существующей функции window.onload init, которую я уже имел в любом случае.

Имейте в виду, что это может помешать пользователям, которые действительно захотят действительно вернуться на любую веб-страницу, на которую они смотрели, перед тем, как перейти к вашей PWA, просматривая ее как стандартную веб-страницу. В этом случае вы можете добавить счетчик, и если пользователь дважды ударит дважды, вы можете фактически разрешить "нормальное" событие возврата (или закрыть приложение).

Chrome на Android также (по какой-то причине) добавил дополнительное состояние пустой истории, поэтому потребовалось еще один 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);
}

Ответ 3

Этот подход имеет несколько улучшений по сравнению с существующими ответами:

Позволяет пользователю выйти, если он дважды нажимает в течение 2 секунд: наилучшая длительность является дискуссионной, но идея использования опции переопределения распространена в приложениях Android, поэтому зачастую это правильный подход.

Включает это поведение только в автономном режиме (PWA). Это обеспечивает работу веб-сайта, ожидаемую пользователем в веб-браузере Android, и применяет этот обходной путь только в том случае, если пользователь видит веб-сайт, представленный как "настоящее приложение".

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);
    });
}