AJAX y el objeto window.history

El objeto history de JavaScript contiene una lista de las URL que hemos visitado en el mismo tab, entre las cuales podemos desplazarnos y acceder a ellas. Antes de la llegada de HTML5 este objeto resultaba inútil de cara a trabajar con aplicaciones basadas en AJAX, teniendo que recurrir a otros métodos menos naturales y limitados. Sin embargo, el desarrollo de su especificación lo ha convertido en el complemento ideal para trabajar con esta tecnología.

Hasta ahora, el objeto history (a su vez propiedad del objeto "window") disponía de tres métodos que nos permitían interaccionar con el historial de navegación de nuestro navegador:

  • back(): carga la URL del estado anterior

  • forward(): carga la URL del estado siguiente

  • go(número): carga la URL del estado indicado en el par& aacute;metro

Y una única propiedad:

  • length: indica el número de elementos disponibles en el historial de navegación

Aunque existían alternativas gracias al evento haschange, estos métodos resultaban insuficentes a la hora de trabajar con páginas cargadas por medio de AJAX, puesto que no había manera de asociar un estado al historial, de manera que al retroceder/avanzar por medio de los botones de nuestro navegador se perdía la información.

Pero el desarrollo del objeto nos ha traido dos nuevos métodos (pushState y replaceState) que solucionan este problema, además de otra nueva propiedad state:

El método pushState añade un nuevo estado a nuestra lista mientras que replaceState sustituye el estado actual. Como veremos, ambos métodos funcionan de forma muy parecida y requiren tres parámetros:

  • state: json que reprenta el estado del nuevo elemento añadido al historial
  • title: titulo del nuevo elemento (no confundir con el contenido del elemento title de html). Este parámetro aún no es soportado en la mayoría de navegadores por lo que le daremos un valor null.

  • url: URL (relativa o absoluta) asociada al nuevo elemento

Por otro lado, la nueva propiedad state devuelve el valor del estado en el que nos encontramos, sin tener que depender del evento popstate.

Aunque la especificación aun está en desarrollo la mayoría de los navegadores modernos ya soportan estos nuevos métodos, a excepción de Internet Explorer (< 10) y Safari (parcialmente). Por cierto, en la página de desarrolladores de Mozilla mencionan otros tres métodos más, que sin embargo no son estándar, razón por la que he decidido ignorarlos.

Para estudiar la implementación de las nuevas características del objeto history en una aplicación AJAX he creado una pequeño ejemplo cuyo repositorio he subido a github. Esta aplicación está desarrollada en PHP y jQuery y consta de los siguientes elementos: el fichero de entrada index.php, el cual contiene la plantilla y se encarga de llamar a las vistas contenidos en la carpeta views; la carpeta js que contiene la librería de jQuery y el fichero file.js que contiene las llamadas a los métodos del objeto history; un fichero .htaccess para forzar el patrón front controller.

Empezamos por este último:

#.htaccess

RewriteEngine on

nRewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php [L]

Aquí simplemente forzamos un único punto de entrada (el fichero index.php) siempre que la url no corresponda a la de un fichero existente en nuestra app.

El fichero index.php:

// index.php

<?php
$uri = explode('/', $_SERVER['REQUEST_URI']);
$view = end($uri);
if (empty($view) || $view === 'index.php') {
    header('location: index');
}
?>
<!DOCTYPE html>
<html lang="es">
    <head>
        <meta charset="UTF-8">
        <title>history Object Example</title>
        &
lt;script src="js/jquery.js"></script>
        <script src="js/file.js"></script>
    </head>
    <body>
        <nav>
            <?php include 'views/'.$view.'.html'; ?>
        </nav>
        <button id="previous">Previous</button>
    
    <button id="forward">Forward</button>
        <p>Number of pages in the history stack: <span><span></p>
    </body>
</html>

El script en PHP identifica el slug de la url para asociar la vista correspondiente. Por defecto será "index". El código HTML contiene la plantilla, la cual está compuesta de un menú de navegación definido por la vista, dos botones que nos permiten movernos a través de nuestro historial de navegación (análogos a los botones atrás y adelante de nuestro navegador) y un texto que indica el número de elementos incluidos la lista.

Las vistas las he hecho lo más sencillas posibles y únicamente contienen el menú de navegación, resaltando el elemento que se corresponde con la propia vista:

<!-- views/index.html -->

<ul>
    <li><strong>index</strong></li>
    <li><a href="page1">page1</a></li>
    <
li><a href="page2">page2</a></li>
</ul>

<!-- views/page1.html  -->

<ul>
    <li><a href="index">index</a></li>
    <li><strong>page1</strong></li>
    <li><a href="page2">page2</a></li>
</ul>

<!-- views/page2.html  -->

<ul>
    <li><a href="index">index</a></li>
    <li><a href="page1">page1</a></li>
    <li><strong>page2</strong>&
lt;/li>
</ul>

Y por último, el fichero file.js que es el que realmente nos interesa:

// js/file.js

$(document).ready 
( 
    function () 
    { 
        $('#previous').click 
        ( 
            function() 
            { 
                window.history.back(); 
            } 
        ); 
        $('#forward').click 
        ( 
    
        function() 
            { 
                window.history.forward(); 
            } 
        ); 
        function loadView(view) 
        { 
            $('nav').load('views/'+view+'.html'); 
        } 
        function printHistoryLength() 
        { 
            $('span').text(window.history.length); 
        } 
        $('nav').on 
        ( 
            'click', 
            'a', 
            function(event) 
            { 
                if (typeof window.history.pushState == '
function') { 
                    event.preventDefault(); 
                    var view = $(this).attr('href'); 
                    window.history.pushState(view, null, view); 
                    loadView(view); 
                    printHistoryLength(); 
                } 
            } 
        ); 
        window.history.replaceState($('strong').text(), null, $('strong').text()); 
        printHistoryLength(); 
r
        window.onpopstate = function(event) 
        { 
            loadView(window.history.state);
        }; 
    } 
);

Vamos a estudiar este documento por partes:

$('#previous').click 
( 
    function() 
    { 
        window.history.back(); 
    } 
); 
$('#forward').click 
( 
    function() 
    { 
        window.history.forward(); 
    } 
); 

Estas funciones asociadas al evento click para los botones #previous y #forward nos permiten simular el comportamiento de los botones del navegador "Atrás" (método window.history.back) y "Adelante" (window.history.forward), respectivamente.

function loadView(view) 
{ 
    $('nav').load('views/'+view+'.html'); 
} 
function printHistoryLength() 
{ 
    $('span').text(window.history.length); 
} 

Para no repetir código, declaramos las funciones loadView y printHistoryLength. La primera carga vía AJAX las vistas contenidas en la carpeta view mediante el método load de jQuery . La segunda simplemente pinta el número de elementos almacenados en nuestro historial de navegación.

 $('nav').on 
( 
    'click', 
    'a', 
    function(event) 
    {
        if (typeof window.history.pushState == 'function') { 
            event.preventDefault(); 
            var view 
= $(this).attr('href'); 
            window.history.pushState(view, null, view); 
            loadView(view); 
            printHistoryLength(); 
        } 
    } 
); 

Aquí me sirvo del método on de jQuery (>= 1.7) necesario para poder asociar el evento click a los enlaces del menú de navegación de las vistas cargadas vía AJAX.

Lo primero que hacemos es comprobar si el navegador soporta el método history.pushState. De ser así, invocamos dicho método al que pasamos a los parámetros "state" y "url" la variable url, la cual corresponde con el slug al que apunta el enlace en cuestión. Como no vamos a utilizar el parámetro "title", le damos un valor nulo. Una vez añadido el nuevo estado, podemos observar como por arte de magia la URL de nuestro navegador cambia al valor dado. A continuación llamamos a las funciones loadView, a la que le pasamos como parámetro dicho slug, y printHistoryLength.

En caso de que el navegador no soporte el método, la aplicación se comportará de manera natural recargando la página al abrir el nuevo enlace .

window.history.replaceState($('strong').text(), null, $('strong').text()); 
printHistoryLength(); 

Estas dos líneas son necesarias al cargar la página inicial. Para poder asignarle un estado (por defecto siempre es null) debemos invocar el método history.replaceState pasando el valor del slug de la página actual a los parámetros "state" y "url". De lo contrario no podríamos acceder a ella por medio del historial de navegación. A continuación llamamos a la función printHistoryLength para pintar el número de sitios almacenados en la lista. Notar que si en su lugar hubieramos utilizado el método replaceState, al recargar la página el número de sitios almacenados aumentaría en +1 (algo que en un principio no debería interesarnos).

window.onpopstate = function(event) 
{ 
    loadView(window.history.state);
};

Por último definimos la función asociada al evento popstate, la cual se lanzará cada vez que naveguemos a través del historial. Hay que tener en cuenta que para poder visualizar el contenido de cada estado almacenado debemos llamar a la función loadView y pasarle el slug como parámetro, de lo contrario podríamos desplazarnos por el historial pero, logicamente, no se cargaría la vista asociada. Es decir, lo que en realidad hacemos es simular las acciones de retroceder/avanzar entre las páginas almacenadas en nuestro historial.

Por cierto, señalar que también podríamos haber utilizado la propiedad event.state en lugar de history.state, aunque de esta manera nos ahorramos problemas con Google Chrome. Debemos tener en cuenta que este navegador dispara el evento onpopstate cada vez que se carga una página, de manera que en nuestro caso, al cargar la página inicial la propiedad event.sate habría devuelto un valor nulo, saltando por lo tanto un error en la aplicación.

Dicho todo esto, debemos saber que siguen dándose algunos problemas. El más obvio, que en Google Chrome el contenido de nuestra página inicial será cargado dos veces (primero por PHP y luego por AJAX debido al evento popstate), lo que no es para nada deseable. Además, también existen incompatibilidades con algunas versiones antiguas de Safari y Opera. Para evitar estos problemas y facilitar su implementación crossbrowser Benjamin Lupton ha creado la libreria history.js que además es compatible con frameworks como jQuery o Mootools.

Comments

You must sing in to post a comment.

There are no comments for this article.