Solución:
Si bien el botón de retroceso de Android no se puede conectar directamente desde el contexto de una aplicación web progresiva, existe una API de historial que podemos usar para lograr el resultado deseado.
En primer lugar, cuando no hay historial del navegador para la página en la que se encuentra el usuario, al presionar el botón Atrás se cierra inmediatamente la aplicación.
Podemos evitar esto agregando un estado de historial anterior cuando la aplicación se abre por primera vez:
window.addEventListener('load', function() {
window.history.pushState({}, '')
})
La documentación para esta función se puede encontrar en mdn:
pushState () toma tres parámetros: un objeto de estado, un título (que actualmente se ignora) y (opcionalmente) una URL[…] si no se especifica, se establece en la URL actual del documento.
Entonces, ahora el usuario tiene que presionar el botón Atrás dos veces. Una pulsación nos devuelve al estado del historial original, la siguiente pulsación cierra la aplicación.
La segunda parte es que nos conectamos al evento popstate de la ventana que se activa cada vez que el navegador navega hacia atrás o hacia adelante en el historial a través de una acción del usuario (por lo tanto, no cuando llamamos a history.pushState).
Se envía un evento popstate a la ventana cada vez que la entrada activa del historial cambia entre dos entradas del historial para el mismo documento.
Entonces ahora tenemos:
window.addEventListener('load', function() {
window.history.pushState({}, '')
})
window.addEventListener('popstate', function() {
window.history.pushState({}, '')
})
Cuando se carga la página, creamos inmediatamente una nueva entrada en el historial, y cada vez que el usuario presiona ‘atrás’ para ir a la primera entrada, ¡agregamos la nueva entrada nuevamente!
Por supuesto, esta solución es tan simple para aplicaciones de una sola página sin enrutamiento. Tendrá que ser adaptado para aplicaciones que ya usan la api del historial para mantener la url actual sincronizada con el lugar donde navega el usuario.
Para hacer esto, agregaremos un identificador al objeto de estado del historial. Esto nos permitirá aprovechar el siguiente aspecto de la popstate
evento:
Si la entrada del historial activada se creó mediante una llamada a history.pushState (), […] La propiedad de estado del evento popstate contiene una copia del objeto de estado de la entrada del historial.
Así que ahora durante nuestro popstate
manejador podemos distinguir entre la entrada del historial que estamos usando para evitar el comportamiento del botón de retroceso-cierra-la aplicación frente a las entradas del historial que se usan para el enrutamiento dentro de la aplicación, y solo reenviar nuestra entrada del historial preventivo cuando se ha hecho estallar específicamente:
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 }, '')
}
})
El comportamiento final observado es que cuando se presiona el botón Atrás, retrocedemos en el historial del enrutador de nuestra aplicación web progresiva o permanecemos en la primera página que se vio cuando se abrió la aplicación.
@alecdwm, ¡eso es pura genialidad!
No solo funciona en Android (en Chrome y el navegador Samsung), también funciona en navegadores web de escritorio. Lo probé en Chrome, Firefox y Edge en Windows, y es probable que los resultados sean los mismos en Mac. No probé IE porque eew. Incluso si está diseñando principalmente para dispositivos iOS que no tienen botón de retroceso, sigue siendo una buena idea asegurarse de que los botones de retroceso de Android (y Windows Mobile … awww … pobre Windows Mobile) se manejen para que la PWA se sienta mucho más como una aplicación nativa.
Adjuntar un detector de eventos al evento de carga no funcionó para mí, así que hice trampa y lo agregué a una función de inicio window.onload existente que ya tenía de todos modos.
Tenga en cuenta que podría frustrar a los usuarios que realmente querrían volver a la página web que estaban viendo antes de navegar a su PWA mientras la navegan como una página web estándar. En ese caso, puede agregar un contador y si el usuario devuelve el golpe dos veces, puede permitir que suceda el evento de retroceso “normal” (o permitir que la aplicación se cierre).
Chrome en Android también (por alguna razón) agregó un estado de historial vacío adicional, por lo que se necesitó un Atrás adicional para volver atrás. Si alguien tiene alguna idea sobre eso, tendría curiosidad por saber la razón.
Aquí está mi código anti-frustración:
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);
}
Este enfoque tiene un par de mejoras sobre las respuestas existentes:
Permite al usuario salir si presiona hacia atrás dos veces en 2 segundos.: La mejor duración es discutible, pero la idea de permitir una opción de anulación es común en las aplicaciones de Android, por lo que a menudo es el enfoque correcto.
Solo habilita este comportamiento cuando está en modo independiente (PWA): Esto asegura que el sitio web siga comportándose como el usuario esperaría cuando esté dentro de un navegador web de Android y solo aplica esta solución cuando el usuario ve el sitio web presentado como una “aplicación real”.
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);
});
}