> Manuales > Taller de HTML5

Qué es el API History de HTML5. Cómo podemos usar los métodos pushState y replaceState del objeto history y el evento popstate para controlar los cambios en el historial del navegador.

El API History de HTML5

Crear sitios web rápidos y funcionales es un reto que casi todos los desarrolladores web conocen bien. La carga de cualquier página nueva cuando un usuario pulsa sobre un enlace es lenta. Si todo el contenido que se muestra es dinámico, el botón "página anterior", en la práctica de poco sirve. Incluso es antiproducente porque no produce el cambio de estado o "vista" que un usuario esperararía.

Trabajar con hashes es mejor, pero no es lo ideal. Los navegadores actuales evitan el problema, ya que soportan la funcionalidad llamada Historial HTML5. Las APIs pushState, replaceState y popstate proporcionan un control detallado sobre el funcionamiento del botón de página anterior y el URL que se muestra al usuario en contenidos dinámicos. Estas APIs en conjunto nos ayudan a mejorar el rendimiento de nuestros sitios web sin afectar a su usabilidad.

En un artículo de DesarrolloWeb ya se ha tratado el objeto historial de manera genérica, pero aún existen cosas que podemos mejorar si se conoce el API History de HTML5, algo que vamos a explicar en este artículo.

pushState y replaceState

Puedes hacerte a la idea de que pushState viene a ser el equivalente dinámico de la navegación a otra página. En este mismo sentido, replaceState es prácticamente lo mismo que location.replace. La diferencia es que estas APIs dejan intacto el contenido de la página cuando se actualiza el histórico de sesión, ya que almacena estados, en vez de páginas. En otras palabras, estos métodos del objeto history no recargan el contenido de la página.

Tanto pushState como replaceState tienen tres parámetros:

history.pushState(data, title, url);
history.replaceState(data, title, url);

Conviene saber que la mayoría de navegadores, incluyendo IE10, ignoran el parámetro title de pushState y replaceState. Si quieres, puedes seguir manteniendo esta información, ya que en versiones futuras de los navegadores podría exponerse dentro de su interfaz de usuario del historial.

Configuración de URLs personalizados

El parámetro URL de pushState y replaceState puede utilizarse para actualizar el URL de la página sin abrirla, es decir, sin navegar a ningún lugar.

Para dar una idea de cómo funciona, imagina que has cargado la página www.contoso.com/index.html. Usando cambios de hash, solo puedes añadir texto al URL:

// Cambiar a "http://www.contoso.com/index.html#about.html"
location.hash = "about.html";

Pero con pushState y replaceState puedes apuntar a una página totalmente distinta de tu sitio web sin tener que ir realmente a ella:

// Cambiar a "http://www.contoso.com/about.html"
history.pushState(null, "About", "/about.html");

Tienes que asegurarte de que tu servidor puede manejar todos los URLs dinámicas que creas, para tener certeza de que cosas como los enlaces a favoritos funcionarán bien, o que los usuarios puedan compartir una URL generada en redes sociales y les lleve a un sitio lógico a las personas que sigan esos enlaces.

También puedes añadir más datos al objeto de estado de manera que no tengas que interpretar todo el URL más adelante, para recuperar el estado original de la página:

history.pushState("about.html", "About", "/about.html");

El protocolo, hostname y puerto tienen que coincidir con el URL actual, pero el path, query string y fragmento pueden personalizarse a voluntad. Así podemos asociar estados dinámicos con URLs que el servidor puede preparar fácilmente y funcionan aunque el script esté desactivado. Al final, lo que nos permite es obtener y mostrar de forma dinámica solo los datos que han cambiado de una página a otra pero manteniendo intacta la experiencia de usuario.

Restauración de estados guardados

Ahora vamos a ver cómo recuperar el estado después de navegar por el historial (por ejemplo, cuando el usuario pulsa el botón de página anterior) o de volver a cargar la página.

La recuperación del estado se consigue escuchando el evento popstate. El evento popstate se dispara cuando un estado cambia como consecuencia de la navegación por el historial.

En este caso, el objeto de datos para el estado de destino puede recuperarse utilizando history.state. Cuando lo que ocurre es que la página se recarga, el evento popstate no se dispara, pero podemos seguir accediendo a history.state en todo momento, durante o después de la carga. Por tanto, un código como el que vemos en este ejemplo puede recuperar el estado al momento anterior que nos interese:

function init() {
   /* ... */
   // Maneja la carga y recarga de la página
   loadState();
   // Atiende a la navegación por el histórico (p.ej. botón de página anterior)
   window.addEventListener("popstate", loadState, false);
}
function loadState() {
   // Obtiene datos del estado actual para poder recuperarlos más tarde
   var state = history.state;
   /* ... */
}
init();

Almacenamiento de datos dinámicos complejos

El objeto de datos almacenado en un estado puede ser bastante más que una cadena de texto. Podemos utilizar también objetos JavaScript personalizados e incluso algunos tipos nativos, como ImageData. Los datos obtenidos se guardan utilizando el algoritmo estructurado de clonación, que conserva complejas interrelaciones como por ejemplo ciclos y múltiples referencias al mismo objeto.

Así se consigue que guardar y recuperar objetos, incluso los más complejos, sea una tarea sencilla, como se puede ver en esta demo de ejemplo.

En esta demo, se capturan instantáneas del estado del canvas para crear una pila de reversión de acciones ("undo", o "deshacer") con un código como este:

function init() {
   /* ... */
   // Maneja la carga y recarga de la página
   loadState();
   // Atiende a la navegación por el histórico (p.ej. botón de página anterior)
   window.addEventListener("popstate", loadState, false);
}
/* ... */
function stopDrawing() {
   // Obtiene una instantánea del estado actual en forma de instancia ImageData
   var state = context.getImageData(0, 0, canvas.width, canvas.height);
   history.pushState(state, "");
   /* ... */
}
function loadState() {
   // Obtiene datos del estado actual para poder recuperarlos más tarde
   var state = history.state;
   /* ... */
   if (state) {
      // Recupera el canvas al estado de ImageData guardado
      context.putImageData(state, 0, 0);
   }
}

Si quieres cambiarlo para que mantenga el estado actual sin almacenar todos y cada uno de los cambios, puedes utilizar replaceState en lugar de pushState.

La cantidad de datos

El History de HTML5 en seguida envía grandes cantidades de datos a la pila si no tenemos cuidado. Por ejemplo, la demo "undo" anterior llega a almacenar 0,5 Mb por estado y aún puede ser mayor si el canvas aumenta de tamaño.

Esta cantidad de datos consume memoria, ya que cada entrada de estado asociada se mantiene en el historial de sesión, que puede llegar a ser bastante grande antes de que salgamos de la página. Cuantos más datos guardemos, más pronto puede empezar el navegador a eliminar entradas para ahorrar espacio. Además, algunos navegadores imponen un límite máximo para la cantidad de datos que se pueden guardar en una sola llamada a pushState o replaceState.

Compatibilidad con navegadores

Como siempre, tendremos que utilizar la función de detección de funcionalidades para gestionar las diferencias de soporte entre los distintos navegadores. Puesto que casi todo el API History de HTML5 involucra eventos y propiedades, las únicas partes nuevas que realmente exigen detección son las llamadas a pushState y replaceState:

function stopDrawing() {
   var state = context.getImageData(0, 0, canvas.width, canvas.height);
   if (history.pushState)
      history.pushState(state, "");
      /* ... */
}

Este tipo de detección, como poco va a conseguir que tu script no falle en navegadores antiguos. Dependiendo del escenario de uso, puede que te interese empezar con navegaciones de página completa y actualizar a contenidos dinámicos cuando el History de HTML5 esté soportado. Si no, también puedes utilizar un marco de historial o un polyfill para hacer que el botón de página anterior siga funcionando, pero siempre teniendo en cuenta que no todo se puede emular. Por ejemplo, el control dinámico sobre el path y los componentes de la query string de un URL solo se puede conseguir utilizando pushState y replaceState.

Por otra parte, algunos navegadores soportan una especificación draft anterior de History de HTML5, con dos diferencias importantes con respecto al borrador actual:

Así que para soportar estos navegadores, puedes hacer un fallback para leer la información de estado que devuelve el propio evento popstate.

Utilidad

Sin duda, las APIs History de HTML5 nos ofrecen una gran flexibilidad a la hora de crear sitios web manejables y con buena respuesta. Teniendo un cierto cuidado con los navegadores anteriores, estas APIs se pueden utilizar ya para conseguir efectos muy interesantes. Puedes empezar a probarlos en tu sitio web.

Tony Ross

Program Manager, Internet Explorer

Manual