Sencilla web con navegación AJAX en jQuery

No hace mucho que comencé a trabajar en un proyecto de un cliente que quería que toda la navegación del sitio web fuera a través de AJAX para no dar la sensación de recarga de página cada vez que se pulsaran en los enlaces y para mantener ciertos elementos interactivos siempre funcionando, independientemente de la página en la que uno se encontrara.

La experiencia está siendo bastante enriquecedora porque hay bastantes cosas que salvar.

En este artículo vamos a ver cómo crear un sencillo sitio web con navegación AJAX usando jQuery. Vamos a usar tan solo HTML aunque podríamos usar PHP para servir el contenido, y vamos a permitir que funcione con el historial del navegador.

Si quieres, descarga los archivos de la demo, y sigue el tutorial paso a paso.

El marcado HTML

Lo primero que vamos a hacer es crear el marcado HTML. El esqueleto nos servirá para todas las páginas y, en los archivos, podréis ver que tienen distinto contenido.

index.html

<div id="wrapper">  
  <div id="header">
    <h1>Sencilla web con navegación Ajax</h1>
    <h2>Con HTML5 y jQuery</h2>
    <ul id="nav">
      <li><a href="index.html" data-hash="#index">Inicio</a></li>
      <li><a href="pagina1.html" data-hash="#pagina1">Página 1</a></li>
      <li><a href="pagina2.html" data-hash="#pagina2">Página 2</a></li>
      <li><a href="pagina3.html" data-hash="#pagina3">Página 3</a></li>
      <li><a href="argofifa.html">Este enlace fallará</a></li>
    </ul>
  </div>
  <div class="clearfix"></div>
  <div id="contenido">
    <p>Este es el texto de la página de inicio. Prueba a navegar con los enlaces de la página de arriba.</p>
  </div>   
</div>  

Este código está dentro del elemento body. Nos sirve de esqueleto y contendrá todo el contenido que carguemos dinámicamente con jQuery.

La parte más reseñable es la navegación. Los enlaces llevan a páginas que podéis encontrar en los archivos, pero contienen el atributo data-hash que, básicamente, es el mismo nombre pero sin HTML ni nada. Más tarde nos valdremos de JavaScript para asignar este valor al atributo href del enlace. De esta manera, nos aseguramos de que la página funciona aunque JavaScript esté desactivado.

¿Y por qué usar hashes?

Los hashes nos permiten añadir entradas en el historial del navegador, sin refrescar la página. Esto podríamos conseguirlo con pushState pero ahora mismo su soporte en navegadores no es muy elevado. Además, revisando esta parte de la URL con JavaScript, podemos cambiar la página cargada a través de AJAX. Esto nos permite que, si pasamos la URL a alguien, llegue a la URL a la que queremos que llegue.

El CSS

Echemos un vistazo al CSS de nuestro archivo. Como veis es bastante sencillo:

style.css

#wrapper {
  background: white;
  width: 960px;
  margin: 40px auto 10px auto;
  padding: 15px;
  -webkit-border-radius: 15px;
  -moz-border-radius: 15px;
  border-radius: 15px;
  -webkit-box-shadow: 0px 1px 5px 1px #38466F;
  -moz-box-shadow: 0px 1px 5px 1px #38466F;
  box-shadow: 0px 1px 5px 1px #38466F;
}
#header > h2, #header > h1 {
  margin: 0;
}
#header > h2 {
  color: #303647;
}
#header > h1 {
  color: #071337;
}
#nav {
  margin: 20px 0;
  width: 100%;
  display: block;
  padding: 0;
}
#nav > li {
  float: left;
  list-style: none;
}
#nav > li > a {
  padding: 5px 10px;
  background: #38466F;
  border: solid 1px #071337;
  color: #E1E6F5;
  -moz-border-radius: 5px;
  -khtml-border-radius: 5px;
  -webkit-border-radius: 5px;
  border-radius: 5px;
  display: block;
  margin-right: 10px;
  background: -moz-linear-gradient(top, #38466f 0%, #303647 100%);
  /* FF3.6+ */

  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #38466f), color-stop(100%, #303647));
  background: -webkit-linear-gradient(top, #38466f 0%, #303647 100%);
  /* Chrome10+,Safari5.1+ */

  background: -o-linear-gradient(top, #38466f 0%, #303647 100%);
  /* Opera 11.10+ */

  background: -ms-linear-gradient(top, #38466f 0%, #303647 100%);
  /* IE10+ */

  background: linear-gradient(top, #38466f 0%, #303647 100%);
  /* W3C */

  filter: progid: DXImageTransform.Microsoft.gradient( startColorstr='#38466f', endColorstr='#303647', GradientType=0);
  /* IE6-9 */
}
#nav > li > a:hover {
  background: #303647;
}
#contenido {
  margin-top: 20px;
  border: 1px dotted #CCC;
  padding: 10px;
  -moz-border-radius: 5px;
  -khtml-border-radius: 5px;
  -webkit-border-radius: 5px;
  border-radius: 5px;
}
.ejemplo {
  font-size: 0.6em;
  color: #071337;
}
.ejemplo a, .ejemplo a:visited {
  color: #38466F;
}
.ejemplo a:hover {
  text-decoration: underline;
}

Así va luciendo nuestra web

La parte de jQuery

Esta es quizá la parte más interesante, ya que es la que se encarga de mover el sitio:

(function($){
  var contenido = $('div#contenido'), 
      url_anterior = '', 
      extension = '.html', 
      original = window.location;

  $('ul#nav a').each(function(){ //Cambiamos los href por el contenido del atributo data-hash
    $(this).attr('href', $(this).data('hash'));
  });
  $('ul#nav a').on('click', function(e){          
    var hash = $(this).attr('href'); 
    e.preventDefault();  
    revisarURL(hash).done(function(){
        window.location.href = hash; // Buen hash, cambiemoslo en la URL            
    }).fail(function(){
        window.location.href = '#error';
    }).always(function(datos){
        contenido.html(datos);
    });
  });

  revisarURL().always(function(datos){
    // Si hay un hash en la URL (ej, copiamos y pegamos en una conversación) cargará la URL correcta.
    contenido.html(datos);
  });

  // Revisamos cualquier cambio en el Hash cada 250 milisegundos
  setInterval(function(){
    revisarURL().fail(function(){
      window.location.href = '#error';
    }).always(function(datos){
      contenido.html(datos);
    });
  },250); 

  function revisarURL (hash){
    var deferred = $.Deferred();
    if (!hash) { // Esto ocurre cuando se pulsa el botón de atrás o adelante en el navegador o al pasar una URL con hash
      hash = window.location.hash;
    }
    if (!hash) { // Esto puede pasar si es la primera URL - index.html en nuestro caso
      var url = window.location.pathname; // Obtenemos la URL completa
      var archivo = url.substring(url.lastIndexOf('/')+1); // Nos quedamos con el nombre del archivo (index.html)
      hash = archivo.replace(extension,''); // Le quitamos la extensión para convertirlo en "hash"
    }
    if (hash !== url_anterior){ 
      url_anterior = hash; 
      cargarPagina(hash).done(
        function(data){ 
          var html = $(data);
          var filtrado = html.find('#contenido');
          deferred.resolve(filtrado.html());
        }
      ).fail(function(){ // La URL no existe                  
        deferred.reject('<p>La página no existe.</p>'); // Rechazamos nuestro deferred      
      });
    }
    return deferred.promise(); // Devolvemos una promesa, no un deferred
  }
  function cargarPagina(hash){
    url = hash.replace('#','');
    //Quitamos la almohadilla
    return $.ajax({
      url: url + extension,
      async: true,
      dataType: "html"
    });         
  }
})(jQuery);

Vamos de arriba a abajo.

Lo primero que hacemos es cambiarle a cada uno de los enlaces de la barra de navegación el atributo href, por el contenido de data-hash que, como ya hemos explicado, nos servirá para crear entradas en el historial.

Luego, asociamos a cada elemento a de la navegación, una función gestora del evento click. Lo que hacemos es almacenar el valor del hash, prevenir la navegación y esperar a que termine la función revisarURL, la cual recibe el hash como parámetro. Estamos haciendo uso de los deferred.

Cuando cargue la página, el script revisará la URL (sin hash ninguno) para ver si tiene algún hash ya en ella y, de ser así, cargarlo en vez de mostrar el contenido del inicio.

Lo siguiente que hacemos es establecer un temporizador recurrente que comprueba periódicamente (cada 250 ms) un cambio en la URL, de esta manera, nos aseguramos de que si el usuario pulsa el botón atrás o adelante en el navegador, el contenido de la página cambie de manera acorde.

Vamos al meollo de la cuestión con la función revisarUrl.

Lo primero que hacemos es crear nuestro objeto deferred. En caso de que no pasemos hash a la función, lo sacaremos de la URL. Si aun así no tenemos un hash, el script extrae el nombre del archivo de la barra de direcciones.

Si el hash es distinto a la URL anterior, comienza el proceso de cambio de página en el que nuevamente, volvemos a hacer uso de los deferreds.

Si la carga de la página tiene éxito, filtramos el contenido para obtener únicamente lo almacenado en la div con id de contenido ya que el resto no nos sirve. En caso de error, rechazamos y pasamos un error personalizado, que colocaremos en la página.

La función cargarPagina tan solo hace una petición AJAX normal que devolvemos para aprovecharnos del potencial de los elementos deferred.

Conclusión

Acabamos de crear una sencilla web en muy muy poco tiempo y con un efecto bastante vistoso. Podríamos haber hecho uso de  animaciones a la hora de introducir el contenido, pero he preferido no hacerlo para no extender demasiado el código.

¿Aun tienes alguna duda? ¡No dejes de comentarla en los comentarios!

Programador Front-end en First + Third y Potato. Trabajando con JavaScript y HTML5 desde el corazón de Sevilla.