¿Cómo recibir notificaciones sobre los cambios del historial a través de history.pushState?

154

Entonces, ahora que HTML5 se presenta history.pushStatepara cambiar el historial de los navegadores, los sitios web comienzan a usar esto en combinación con Ajax en lugar de cambiar el identificador de fragmento de la URL.

Lamentablemente, eso significa que esas llamadas ya no se pueden detectar onhashchange.

Mi pregunta es: ¿hay alguna forma confiable (piratear?) Para detectar cuándo se usa un sitio web history.pushState? La especificación no establece nada sobre los eventos que se generan (al menos no pude encontrar nada).
Traté de crear una fachada y la reemplacé window.historycon mi propio objeto JavaScript, pero no tuvo ningún efecto.

Explicación adicional: estoy desarrollando un complemento de Firefox que necesita detectar estos cambios y actuar en consecuencia.
Sé que hubo una pregunta similar hace unos días que preguntaba si escuchar algunos eventos DOM sería eficiente, pero prefiero no confiar en eso porque estos eventos se pueden generar por muchas razones diferentes.

Actualizar:

Aquí hay un jsfiddle (use Firefox 4 o Chrome 8) que muestra que onpopstateno se activa cuando pushStatese llama (¿o estoy haciendo algo mal? ¡Siéntase libre de mejorarlo!).

Actualización 2:

Otro problema (lateral) es que window.locationno se actualiza cuando se usa pushState(pero ya leí sobre esto en SO, creo).

Felix Kling
fuente

Respuestas:

194

5.5.9.1 Definiciones de eventos

El evento popstate se desencadena en ciertos casos cuando se navega a una entrada del historial de sesión.

De acuerdo con esto, no hay ninguna razón para que se dispare popstate cuando se usa pushState. Pero un evento como pushstatesería útil. Debido a que historyes un objeto host, debes tener cuidado con él, pero Firefox parece ser bueno en este caso. Este código funciona bien:

(function(history){
    var pushState = history.pushState;
    history.pushState = function(state) {
        if (typeof history.onpushstate == "function") {
            history.onpushstate({state: state});
        }
        // ... whatever else you want to do
        // maybe call onhashchange e.handler
        return pushState.apply(history, arguments);
    };
})(window.history);

Tu jsfiddle se convierte en :

window.onpopstate = history.onpushstate = function(e) { ... }

Puedes hacer un parche de mono de window.history.replaceStatela misma manera.

Nota: por supuesto, puede agregar onpushstatesimplemente al objeto global, e incluso puede hacer que maneje más eventos a través deadd/removeListener

gblazex
fuente
Dijiste que reemplazaste todo el historyobjeto. Eso puede ser innecesario en este caso.
gblazex
@galambalazs: Sí, probablemente. Quizás (no sé) window.historyes de solo lectura, pero las propiedades del objeto de historial no lo son ... Muchas gracias por esta solución :)
Felix Kling
De acuerdo con las especificaciones, hay un evento de "transición de página", parece que aún no está implementado.
Mohamed Mansour
2
@ user280109 - Te diría si lo supiera. :) Creo que no hay forma de hacer esto en Opera atm.
gblazex
1
@cprcrack Es para mantener limpio el alcance global. Debe guardar el pushStatemétodo nativo para su uso posterior. Entonces, en lugar de una variable global, elegí encapsular todo el código en un IIFE: en.wikipedia.org/wiki/Immediately-invoked_function_expression
gblazex
4

Solía ​​usar esto:

var _wr = function(type) {
    var orig = history[type];
    return function() {
        var rv = orig.apply(this, arguments);
        var e = new Event(type);
        e.arguments = arguments;
        window.dispatchEvent(e);
        return rv;
    };
};
history.pushState = _wr('pushState'), history.replaceState = _wr('replaceState');

window.addEventListener('replaceState', function(e) {
    console.warn('THEY DID IT AGAIN!');
});

Es casi lo mismo que hizo galambalazs .

Sin embargo, generalmente es exagerado. Y podría no funcionar en todos los navegadores. (Solo me importa mi versión de mi navegador).

(Y deja una var _wr, por lo que es posible que desee envolverlo o algo. No me importó eso).

Rudie
fuente
4

Finalmente encontré la forma "correcta" de hacer esto. Requiere agregar un privilegio a su extensión y usar la página de fondo (no solo un script de contenido), pero funciona.

El evento que desea es browser.webNavigation.onHistoryStateUpdated, que se activa cuando una página usa la historyAPI para cambiar la URL. Solo se dispara para los sitios a los que tiene permiso de acceso, y también puede usar un filtro de URL para reducir aún más el correo no deseado si es necesario. Requiere el webNavigationpermiso (y, por supuesto, el permiso del host para los dominios relevantes).

La devolución de llamada del evento obtiene el ID de la pestaña, la URL a la que se está "navegando" y otros detalles similares. Si necesita realizar una acción en la secuencia de comandos de contenido en esa página cuando se dispara el evento, inyecte la secuencia de comandos relevante directamente desde la página de fondo o haga que la secuencia de comandos de contenido abra una portpágina de fondo cuando se cargue, guarde la página de fondo ese puerto en una colección indexada por ID de pestaña, y envía un mensaje a través del puerto relevante (desde la secuencia de comandos de fondo a la secuencia de comandos de contenido) cuando se desencadena el evento.

CBHacking
fuente
3

¿Podrías unirte al window.onpopstateevento?

https://developer.mozilla.org/en/DOM%3awindow.onpopstate

De los documentos:

Un controlador de eventos para el evento popstate en la ventana.

Se envía un evento popstate a la ventana cada vez que cambia la entrada activa del historial. Si la entrada del historial que se está activando fue creada por una llamada a history.pushState () o se vio afectada por una llamada a history.replaceState (), la propiedad de estado del evento popstate contiene una copia del objeto de estado de la entrada del historial.

stef
fuente
66
He intentado esto ya y el evento sólo se activa cuando los usuarios se remonta en la historia (o cualquiera de los history.go, .back) se llaman funciones. Pero no en pushState. Aquí está mi intento de probarlo, tal vez hago algo mal: jsfiddle.net/fkling/vV9vd Parece solo relacionado de esa manera que si el historial fue cambiado por pushState, el objeto de estado correspondiente se pasa al controlador de eventos cuando los otros métodos son llamados.
Felix Kling
1
Ah En ese caso, lo único que se me ocurre es registrar un tiempo de espera para ver la longitud de la pila de historial y activar un evento si el tamaño de la pila ha cambiado.
stef
Ok, esta es una idea interesante. Lo único es que el tiempo de espera debería dispararse con la frecuencia suficiente para que el usuario no note ningún retraso (largo) (tengo que cargar y mostrar datos para la nueva URL). Siempre trato de evitar los tiempos de espera y las encuestas cuando sea posible, pero hasta ahora esta parece ser la única solución. Seguiré esperando otras propuestas. Pero muchas gracias por ahora!
Felix Kling
Puede ser incluso suficiente verificar la longitud del historial en cada clic.
Felix Kling
1
Sí, eso funcionaría. He estado jugando con esto: un problema es la llamada replaceState que no lanzaría un evento porque el tamaño de la pila no habría cambiado.
stef
1

Como estás preguntando acerca de un complemento de Firefox, aquí está el código que pude trabajar. Ya no se recomienda el uso y unsafeWindowse producen errores cuando se llama a pushState desde un script de cliente después de ser modificado:

Permiso denegado para acceder al historial de propiedades.pushState

En cambio, hay una API llamada exportFunction que permite que la función se inyecte de window.historyesta manera:

var pushState = history.pushState;

function pushStateHack (state) {
    if (typeof history.onpushstate == "function") {
        history.onpushstate({state: state});
    }

    return pushState.apply(history, arguments);
}

history.onpushstate = function(state) {
    // callback here
}

exportFunction(pushStateHack, unsafeWindow.history, {defineAs: 'pushState', allowCallbacks: true});
nathancahill
fuente
En la última línea, debe cambiar unsafeWindow.history,a window.history. No funcionaba para mí con unsafeWindow.
Lindsay-Needs-Sleep
0

La respuesta de galambalazs parches de mono window.history.pushStatey window.history.replaceState, pero por alguna razón dejó de funcionar para mí. Aquí hay una alternativa que no es tan buena porque usa encuestas:

(function() {
    var previousState = window.history.state;
    setInterval(function() {
        if (previousState !== window.history.state) {
            previousState = window.history.state;
            myCallback();
        }
    }, 100);
})();
Flimm
fuente
¿Hay alguna alternativa para la votación?
SuperUberDuper
@SuperUberDuper: mira las otras respuestas.
Flimm
@Flimm Polling no es agradable, pero feo. Pero bueno, no tenías elección, dijiste.
Yairopro
Esto es mucho mejor que el parche de mono, el parche de mono se puede deshacer con otro script y NO recibe notificaciones al respecto.
Ivan Castellanos
0

Creo que este tema necesita una solución más moderna.

Estoy seguro nsIWebProgressListener estaba por ahí, entonces me sorprende que nadie lo haya mencionado.

De un framecript (para compatibilidad de e10s):

let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW | Ci.nsIWebProgress.NOTIFY_LOCATION);

Luego escuchando en el onLoacationChange

onLocationChange: function onLocationChange(webProgress, request, locationURI, flags) {
       if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT

Aparentemente eso atrapará todos los pushState. Pero hay un comentario que advierte que "TAMBIÉN se dispara para pushState". Por lo tanto, necesitamos hacer un poco más de filtrado aquí para asegurarnos de que solo se trate de elementos de estado de inserción.

Basado en: https://github.com/jgraham/gecko/blob/55d8d9aa7311386ee2dabfccb481684c8920a527/toolkit/modules/addons/WebNavigation.jsm#L18

Y: recurso: //gre/modules/WebNavigationContent.js

Noitidart
fuente
Esta respuesta no tiene nada que ver con el comentario, a menos que me equivoque. ¿Los WebProgressListeners son para extensiones FF? Esta pregunta es sobre el historial HTML5
Kloar
@Kloar permite eliminar estos comentarios por favor
Noitidart
0

Bueno, veo muchos ejemplos de reemplazo de la pushStatepropiedad de, historypero no estoy seguro de que sea una buena idea, preferiría crear un evento de servicio basado en una API similar al historial para que pueda controlar no solo el estado de inserción sino también el estado de reemplazo también y abre puertas para muchas otras implementaciones que no dependen de la API de historial global. Por favor, consulte el siguiente ejemplo:

function HistoryAPI(history) {
    EventEmitter.call(this);
    this.history = history;
}

HistoryAPI.prototype = utils.inherits(EventEmitter.prototype);

const prototype = {
    pushState: function(state, title, pathname){
        this.emit('pushstate', state, title, pathname);
        this.history.pushState(state, title, pathname);
    },

    replaceState: function(state, title, pathname){
        this.emit('replacestate', state, title, pathname);
        this.history.replaceState(state, title, pathname);
    }
};

Object.keys(prototype).forEach(key => {
    HistoryAPI.prototype = prototype[key];
});

Si necesitas el EventEmitter definición, el código anterior se basa en el emisor de eventos NodeJS: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/events.js . utils.inheritsLa implementación se puede encontrar aquí: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/util.js#L970

Victor Queiroz
fuente
Acabo de editar mi respuesta con la información solicitada, por favor verifique!
Victor Queiroz
@VictorQueiroz Que una idea agradable y limpia para encapsular las funciones. Pero si alguna biblioteca de terceros requiere pushState, su HistoryApi no será notificado.
Yairopro
0

Prefiero no sobrescribir el método de historial nativo, por lo que esta implementación simple crea mi propia función llamada estado eventedPush que solo envía un evento y devuelve history.pushState (). De cualquier manera funciona bien, pero esta implementación me parece un poco más limpia, ya que los métodos nativos continuarán funcionando como esperan los futuros desarrolladores.

function eventedPushState(state, title, url) {
    var pushChangeEvent = new CustomEvent("onpushstate", {
        detail: {
            state,
            title,
            url
        }
    });
    document.dispatchEvent(pushChangeEvent);
    return history.pushState(state, title, url);
}

document.addEventListener(
    "onpushstate",
    function(event) {
        console.log(event.detail);
    },
    false
);

eventedPushState({}, "", "new-slug"); 
jopfre
fuente
0

Basado en la solución dada por @gblazex , en caso de que desee seguir el mismo enfoque, pero utilizando las funciones de flecha, siga el ejemplo a continuación en su lógica de JavaScript:

private _currentPath:string;    
((history) => {
          //tracks "forward" navigation event
          var pushState = history.pushState;
          history.pushState =(state, key, path) => {
              this._notifyNewUrl(path);
              return pushState.apply(history,[state,key,path]); 
          };
        })(window.history);

//tracks "back" navigation event
window.addEventListener('popstate', (e)=> {
  this._onUrlChange();
});

Luego, implemente otra función _notifyUrl(url)que active cualquier acción requerida que pueda necesitar cuando se actualice la URL de la página actual (incluso si la página no se ha cargado)

  private _notifyNewUrl (key:string = window.location.pathname): void {
    this._path=key;
    // trigger whatever you need to do on url changes
    console.debug(`current query: ${this._path}`);
  }
Alberto S.
fuente
0

Como solo quería la nueva URL, he adaptado los códigos de @gblazex y @Alberto S. para obtener esto:

(function(history){

  var pushState = history.pushState;
    history.pushState = function(state, key, path) {
    if (typeof history.onpushstate == "function") {
      history.onpushstate({state: state, path: path})
    }
    pushState.apply(history, arguments)
  }
  
  window.onpopstate = history.onpushstate = function(e) {
    console.log(e.path)
  }

})(window.history);
Andre Lopes
fuente
0

No creo que sea una buena idea modificar las funciones nativas, incluso si puede, y siempre debe mantener el alcance de su aplicación, por lo que un buen enfoque es no usar la función pushState global, en su lugar, use una propia:

function historyEventHandler(state){ 
    // your stuff here
} 

window.onpopstate = history.onpushstate = historyEventHandler

function pushHistory(...args){
    history.pushState(...args)
    historyEventHandler(...args)
}
<button onclick="pushHistory(...)">Go to happy place</button>

Tenga en cuenta que si algún otro código usa la función pushState nativa, no obtendrá un desencadenador de evento (pero si esto sucede, debe verificar su código)

Maxwell sc
fuente
-5

Como estados estándar:

Tenga en cuenta que solo llamar a history.pushState () o history.replaceState () no activará un evento popstate. El evento popstate solo se activa al hacer una acción del navegador, como hacer clic en el botón Atrás (o llamar al historial.back () en JavaScript)

necesitamos llamar a history.back () para activar WindowEventHandlers.onpopstate

Tan insted de:

history.pushState(...)

hacer:

history.pushState(...)
history.pushState(...)
history.back()
usuario2360102
fuente