¿Cómo puedo eliminar un estado de window.history?

10 minutos de lectura

Avatar de usuario de DanielST
DanielST

Usando el HTML5 window.history API, puedo controlar bastante bien la navegación en mi aplicación web.

La aplicación actualmente tiene dos estados: selectDate (1) y enterDetails (2).

Cuando se carga la aplicación, yo replaceState y establecer un popState oyente:

history.replaceState({stage:"selectDate",...},...);
window.onpopstate = function(event) {
    that.toStage(event.state.stage);
};

Cuando se selecciona una fecha y la aplicación pasa a la etapa 2, empujo el estado 2 a la pila:

history.pushState({stage:"enterDetails",...},...);

Este estado se reemplaza cada vez que cambian los detalles para que se guarden en el historial.

Hay tres formas de salir de la etapa 2:

  • guardar (enviar AJAX)
  • Cancelar
  • botón de retroceso

El botón Atrás es manejado por el popstate oyente. El botón de cancelar empuja etapa 1 para que el usuario pueda volver a los detalles que estaba ingresando en el botón Atrás. Ambos funcionan bien.

El botón Guardar debería volver a la etapa 1 y no permitir que el usuario vuelva a navegar a la página de detalles (ya que ya lo enviaron). Básicamente, debería hacer que la pila de historial tenga una longitud = 1.

Pero no parece haber un history.delete()o history.merge(). Lo mejor que puedo hacer es un history.replaceState(stage1) lo que deja la pila de historial como: ["selectDate","selectDate"].

¿Cómo me deshago de una capa?

Editar:

Pensé en otra cosa, pero tampoco funciona.

history.back(); //moves history to the correct position
location.href = "#foo"; // successfully removes ability to go 'forward', 
                       // but also adds another layer to the history stack

Esto deja la pila de historial como ["selectDate","selectDate#foo"].

Entonces, como alternativa, ¿hay alguna manera de eliminar la historia ‘hacia adelante’ sin impulsar un nuevo estado?

Avatar de usuario de Mike B.
mike b

Es posible que ya haya seguido adelante, pero… que yo sepa, no hay forma de eliminar una entrada (o estado) del historial.

Una opción que he estado investigando es manejar el historial usted mismo en JavaScript y usar el window.history objeto como una especie de portador.

Básicamente, cuando la página se carga por primera vez, crea su objeto de historial personalizado (iremos con una matriz aquí, pero use lo que tenga sentido para su situación), luego haga su inicial pushState. Pasaría su objeto de historial personalizado como el objeto de estado, ya que puede ser útil si también necesita manejar a los usuarios que navegan fuera de su aplicación y regresan más tarde.

var myHistory = [];

function pageLoad() {
    window.history.pushState(myHistory, "<name>", "<url>");

    //Load page data.
}

Ahora, cuando navega, agrega a su propio objeto de historial (o no, ¡el historial ahora está en sus manos!) y usa replaceState para mantener el navegador fuera del bucle.

function nav_to_details() {
    myHistory.push("page_im_on_now");
    window.history.replaceState(myHistory, "<name>", "<url>");

    //Load page data.
}

Cuando el usuario navegue hacia atrás, accederá a su estado “base” (su objeto de estado será nulo) y podrá manejar la navegación de acuerdo con su objeto de historial personalizado. Luego, haces otro pushState.

function on_popState() {
    // Note that some browsers fire popState on initial load,
    // so you should check your state object and handle things accordingly.
    // (I did not do that in these examples!)

    if (myHistory.length > 0) {
        var pg = myHistory.pop();
        window.history.pushState(myHistory, "<name>", "<url>");

        //Load page data for "pg".
    } else {
        //No "history" - let them exit or keep them in the app.
    }
}

El usuario nunca podrá navegar hacia adelante usando los botones de su navegador porque siempre está en la página más nueva.

Desde la perspectiva del navegador, cada vez que “retroceden”, inmediatamente vuelven a avanzar.

Desde la perspectiva del usuario, puede navegar hacia atrás a través de las páginas pero no hacia adelante (básicamente simulando el modelo de “pila de páginas” del teléfono inteligente).

Desde la perspectiva del desarrollador, ahora tiene un alto nivel de control sobre cómo el usuario navega a través de su aplicación, al mismo tiempo que le permite usar los botones de navegación familiares en su navegador. Puede agregar/eliminar elementos de cualquier parte de la cadena de historial como desee. Si usa objetos en su matriz de historial, también puede rastrear información adicional sobre las páginas (como contenido de campo y otras cosas).

Si necesita manejar la navegación iniciada por el usuario (como el usuario que cambia la URL en un esquema de navegación basado en hash), entonces puede usar un enfoque ligeramente diferente como…

var myHistory = [];

function pageLoad() {
    // When the user first hits your page...
    // Check the state to see what's going on.

    if (window.history.state === null) {
        // If the state is null, this is a NEW navigation,
        //    the user has navigated to your page directly (not using back/forward).

        // First we establish a "back" page to catch backward navigation.
        window.history.replaceState(
            { isBackPage: true },
            "<back>",
            "<back>"
        );

        // Then push an "app" page on top of that - this is where the user will sit.
        // (As browsers vary, it might be safer to put this in a short setTimeout).
        window.history.pushState(
            { isBackPage: false },
            "<name>",
            "<url>"
        );

        // We also need to start our history tracking.
        myHistory.push("<whatever>");

        return;
    }

    // If the state is NOT null, then the user is returning to our app via history navigation.

    // (Load up the page based on the last entry of myHistory here)

    if (window.history.state.isBackPage) {
        // If the user came into our app via the back page,
        //     you can either push them forward one more step or just use pushState as above.

        window.history.go(1);
        // or window.history.pushState({ isBackPage: false }, "<name>", "<url>");
    }

    setTimeout(function() {
        // Add our popstate event listener - doing it here should remove
        //     the issue of dealing with the browser firing it on initial page load.
        window.addEventListener("popstate", on_popstate);
    }, 100);
}

function on_popstate(e) {
    if (e.state === null) {
        // If there's no state at all, then the user must have navigated to a new hash.

        // <Look at what they've done, maybe by reading the hash from the URL>
        // <Change/load the new page and push it onto the myHistory stack>
        // <Alternatively, ignore their navigation attempt by NOT loading anything new or adding to myHistory>

        // Undo what they've done (as far as navigation) by kicking them backwards to the "app" page
        window.history.go(-1);

        // Optionally, you can throw another replaceState in here, e.g. if you want to change the visible URL.
        // This would also prevent them from using the "forward" button to return to the new hash.
        window.history.replaceState(
            { isBackPage: false },
            "<new name>",
            "<new url>"
        );
    } else {
        if (e.state.isBackPage) {
            // If there is state and it's the 'back' page...

            if (myHistory.length > 0) {
                // Pull/load the page from our custom history...
                var pg = myHistory.pop();
                // <load/render/whatever>

                // And push them to our "app" page again
                window.history.pushState(
                    { isBackPage: false },
                    "<name>",
                    "<url>"
                );
            } else {
                // No more history - let them exit or keep them in the app.
            }
        }

        // Implied 'else' here - if there is state and it's NOT the 'back' page
        //     then we can ignore it since we're already on the page we want.
        //     (This is the case when we push the user back with window.history.go(-1) above)
    }
}

  • Hey gracias. Eso es bastante más elegante que lo que he estado usando como solución alternativa.

    – DanielST

    24 de marzo de 2015 a las 13:09

  • Qué hacer cuando un usuario está acostumbrado a este comportamiento, luego navega varias veces creando un estado, luego navega a un sitio externo, luego navega hacia atrás y asumo que el historial de toda la navegación de la aplicación ya no está y el botón Atrás será último en el montón.

    –Billy Luna

    17 de junio de 2016 a las 18:53

  • El estado impulsado por history.pushState estará disponible cuando regresen (también si cierran o actualizan su navegador). Algunos navegadores activarán un evento popstate al cargar la página, pero otros no (Firefox, por ejemplo). Pero siempre puedes leer history.state directamente. Si no está utilizando el objeto history.state, deberá utilizar otro medio para mantener su estado (como localStorage). history.state se conserva automáticamente como parte del historial de navegación.

    – mike b

    17 de junio de 2016 a las 19:03

  • Cómo insertar la página actual en myHistory como dijiste myHistory.push("page_im_in_now")? Dime un ejemplo por favor

    – Señor perfecto

    5 de diciembre de 2016 a las 6:32

  • ¡Buena solución! Un problema menor podría ser la incapacidad de retroceder más de un paso a la vez. El historial del navegador siempre mostrará solo dos entradas para dicha aplicación.

    – Andrej

    6 mayo 2017 a las 22:17

No hay forma de borrar o leer el historial pasado.

Puede intentar evitarlo emulando la historia en su propia memoria y llamando history.pushState cada vez que la ventana popstate se emite un evento (que es propuesto por la respuesta de Mike actualmente aceptada), pero tiene muchas desventajas que resultarán en una UX aún peor que no admitir el historial del navegador en su aplicación web dinámica, porque:

  • El evento popstate puede ocurrir cuando el usuario retrocede ~ 2-3 estados al pasado
  • El evento popstate puede ocurrir cuando el usuario avanza

Entonces, incluso si intenta evitarlo creando un historial virtual, es muy probable que también pueda conducir a una situación en la que tenga estados de historial en blanco (en los que retroceder/avanzar no hace nada), o donde retroceder/adelante salta algunos de sus estados de la historia totalmente.

  • Otro problema cuando se trata de hacer que avance/retroceda, es que cuando se presiona el estado, el título se guarda en Historial. Ahora, cuando un usuario usa Ver historial o hace clic con el botón derecho en el botón Atrás, el usuario puede ser llevado a una página que no esperaba si intenta hacer algo inteligente.

    – gato robótico

    6 de mayo de 2019 a las 3:27

  • Algunos de estos problemas son bastante triviales de resolver. Indexar el estado empujado es un buen comienzo, algo así como un stateDepth propiedad que se incrementa en el decorado pushStatejunto a un prevState. Si prevState.stateDepth === 4y ahora estás en state.stateDepth === 2tienes una buena idea de lo lejos que estás

    –Nick Bull

    10 de febrero de 2021 a las 11:47

Avatar de usuario de Avatar
Avatar

Una solución sencilla:

var ismobilescreen = $(window).width() < 480;
var backhistory_pushed = false;

$('.editbtn').click( function()
{
    // push to browser history, so back button will close the editor 
    // and not navigate away from site
    if (ismobilescreen && !backhistory_pushed)
    {
        window.history.pushState('forward', null, window.location);
        backhistory_pushed = true;
    }
}

Entonces:

if (window.history && window.history.pushState) 
{
    $(window).on('popstate', function() 
    {
        if (ismobilescreen && backhistory_pushed && $('.editor').is(':visible'))
        {
            // hide editor window (we initiate a click on the cancel button)
            $('.editor:visible .cancelbtn').click();
            backhistory_pushed = false;
        }
    });
}

Resultados en:

  1. El usuario abre el editor DIV, se guarda el estado del historial.
  2. El usuario pulsa el botón Atrás, se tiene en cuenta el estado del historial.
  3. ¡Los usuarios permanecen en la página!
  4. En lugar de navegar hacia atrás, se cierra el editor DIV.

Un problema: si usa un botón “Cancelar” en su DIV y esto oculta el editor, entonces el usuario debe hacer clic en el botón Atrás del dispositivo móvil dos veces para volver a la URL anterior.

Para solucionar este problema puedes llamar window.history.back(); para eliminar la entrada del historial usted mismo que en realidad elimina el estado de acuerdo a lo pedido.

Por ejemplo:

$('.btn-cancel').click( function() 
{
    if (ismobilescreen && backhistory_pushed)
    {
        window.history.back();              
    }
}

Alternativamente, puede insertar una URL en el historial que contiene un ancla, por ejemplo #editor y luego empujar al historial o no si el ancla existe en la URL reciente o no.

¿Ha sido útil esta solución?