Archivo de 'javasctipt'

Historial y botón volver en JavaScript

Cada día son más frecuentes las aplicaciones web que hace un uso extensivo de JavaScript, relegando una mayor carga computacional al cliente, dentro de la arquitectura cliente-servidor, lo que habitualmente se conoce como cliente pesado. Con ello, entre otras cosas, se logra una interfaz de usuario mucho mas rica en prestaciones.

Algunos ejemplos de webs que hacen uso de esto es el cliente de correo de Google Gmail, o las famosas redes sociales como Facebook o Tuenti. En estas aplicaciones, toda la interfaz está gobernada por JavaScript. Cuando accedemos a un enlace, generalmente no conlleva una nueva petición HTTP sino que, en su lugar, el documento actual se actualiza con nuevo contenido (AJAX).

Pero existe un inconveniente: Al hacer las webs así, no se almacenan las actualizaciones de la página en el historial del navegador, de modo que los botones de volver y avanzar del navegador no funcionan como esperaríamos. Esto ocurre porque la URL no ha cambiado, y por tanto, para el navegador no ha habido ningún cambio de página.

Pero en Facebook o GMail, pese a funcionar con JavaScript, sí funciona el botón de volver. ¿Cómo lo hacen? La magia está en el caracter almohadilla (#). Repasemos HTML: Cuando en un documento queremos hacer un enlace a una parte del mismo (Por ejemplo, para saltar a un apartado), lo hacemos con el caracter almohadilla seguido de la palabra clave. Por ejemplo:

<h1 id="top">Inicio</h1>
<p>Un párrafo con mucho contenido...</p>
<a href="#top">Volver arriba</a>

Una de las ventajas de estos enlaces es que no conllevan una nueva petición HTTP, por lo que son inmediatos. Además, el navegador tiene constancia de ellos, por lo que los almacena en el historial. Bien, pues ahí está el truco. Si nos fijamos en una URL de Facebook, veremos que son de este tipo:

http://www.facebook.com/#!/?sk=messages&filter=[fb]unread

Es decir, en este caso el enlace es a la página principal (http://www.facebook.com/) y dentro de ese documento, a un apartado (#!/?sk=messages&filter=[fb]unread). Con JavaScript podemos consultar ese apartado para, en función de él, cargar el contenido que proceda. Ya tenemos todas las piezas para hacer una aplicación web en JavaScript, donde funcionará el historial del navegador, y además podremos guardar las URLs en nuestros marcadores.

Acerca del evento hashchange

Para poder realizar eso, necesitamos un método para que, desde JavaScript, sepamos cuándo se ha actualizado el historial #hash, es decir, para saber cuándo se enlaza a otro apartado dentro del documento actual. En HTML5 queda resuelto esto con el evento windows.onhashchange. Sin embargo, al ser un evento nuevo, no está disponible en todos los navegadores, por lo que se hacen necesarios algunos hacks que permitan que nuestra aplicación funcione en la mayoría de navegadores.

En el caso de utilizar jQuery, el plugin más extendido es jQuery BBQ. Podemos leer más acerca de cómo funciona aqui:
http://benalman.com/projects/jquery-hashchange-plugin/

Implementación básica en jQuery

Este ejemplo es muy sencillo. Únicamente tendremos una lista de enlaces, y marcaremos en negrita al activo, y mostraremos su contenido. Entendiendo eso, podremos aplicarlo a todo lo demás.

En primer lugar, necesitaríamos incluir la biblioteca jquery, y además también el plugin de jquery BBQ.

<!DOCTYPE html>
<html lang="es">
<head>
<script type="text/javascript" src="jquery-1.4.1.js"></script>
<script type="text/javascript" src="jquery.ba-bbq.js"></script>
</head>
<body>
</body>
</html>

Después definimos una lista de enlaces y sus bloques de contenido:

<ul>
    <a href="#enlace1">Enlace 1</a>
    <a href="#enlace2">Enlace 2</a>
    <a href="#enlace3">Enlace 3</a>
    <a href="#enlace4">Enlace 4</a>
</ul>
<div id="enlaces">
    <div id="enlace1">Contenido Enlace 1</div>
    <div id="enlace2">Contenido Enlace 2</div>
    <div id="enlace3">Contenido Enlace 3</div>
    <div id="enlace4">Contenido Enlace 4</div>
</div>

Y ahora el código JavaScript:

$(function(){
  $(window).bind('hashchange', function(e) {
    var url = $.param.fragment();
    $('a.current').removeClass('current');
    if (url) {
        $('a[href="#' + url + '"]').addClass( 'current' );
        $("#enlaces > div").hide();
        $("#"+url).show();
    }
  });

  $(window).trigger( 'hashchange' );

});

El código hace lo siguiente:

  1. En primer lugar definimos una función que irá atada al evento ‘hashchange’. Este evento se dispara, como hemos visto, cada vez que se actualiza el #hash.
  2. En segundo lugar, consultamos el parámetro #hash y lo almacenamos en la variable ‘url’.
  3. Desactivamos el enlace previamente activo.
  4. En caso de que esté definido el parámetro #hash:
    1. Activamos el nuevo enlace activo. Tan sólo le añadimos una clase.
    2. Ocultamos todos los bloques de contenido.
    3. Mostramos el bloque de contenido activo.
  5. Lanzamos el evento hashchange para provocar una actualización, por si en la primera carga de la página ya hubiera un parámetro #hash (Por ejemplo, porque alguien ha guardado una URL en favoritos).

Finalmente, definimos el estilo para la clase “current” y ya lo tenemos.

Podéis probar una demo. Como veis, tras moverte entre los distintos apartados, los botones de Volver y Avance del navegador funcionan. Además, se pueden almacenar las URLs para su futura consulta.

WebApps Working Group

Últimamente leo bastante la página del w3c (Qué acierto fue cambiar su diseño), y casi siempre me veo a mí mismo leyendo sobre el Grupo de Trabajo de Aplicaciones Web (WebApps Working Group). ¿Qué es?

El W3C Web Applications (WebApps) Working Group, una fusión de los grupos de trabajoWebAPI y WAF, se constituye para desarollar APIs estándar para el desarrollo de Aplicaciones Web del lado del cliente. Este trabajo incluirá tanto la documentación de APIs existentes como el XMLHttpRequest, como el desarrollo de nuevas APIs que enriquezcan las aplicaciones web.

Veamos en qué consisten algunas de esas APIs que están desarrollando:

Interfaz para Widgets

Cada widget podrá tener un documento de configuración que almacenará metadatos relativos al Widget. Además, cada instancia de un widget podrá almacenar datos de manera persistente.  ¿Cómo se hará esto? Gracias a otra API de almacenamiento de datos locales.

Está bastante maduro (En estado de Recomendación Candidata), y existe una suite de tests para probar su correcta implementación.

API de selectores Level 1

Supongamos el siguiente bloque de código HTML 4.01:

<table id="score">
  <thead>
    <tr>
      <th>Test
      <th>Result
  <tfoot>
    <tr>
      <th>Average
      <td>82%
  <tbody>
    <tr>
      <td>A
      <td>87%
    <tr>
      <td>B
      <td>78%
    <tr>
      <td>C
      <td>81%
</table>

Si quisiéramos obtener los valores de todas las celdas tendríamos que ejecutar algo similar a esto con la API de DOM Level 2:

var table = document.getElementById("score");
var groups = table.tBodies;
var rows = null;
var cells = [];

for (var i = 0; i < groups.length; i++) {
  rows = groups[i].rows;
  for (var j = 0; j < rows.length; j++) {
    cells.push(rows[j].cells[1]);
  }
}

¡Qué feo! La nueva API permite hacerlo de este modo mucho más conciso:

var cells = document.querySelectorAll("#score>tbody>tr>td:nth-of-type(2)");

Como véis, es exactamente igual que lo que ya hacemos con los selectores de jQuery, salvo que en un tiempo podremos realizarlo directamente desde el motor de JavaScript del navegador, sin necesidad de frameworks.

Esta API también está muy madura, y al igual que la anterior es una Recomendación Candidata. Está disponible en Firefox desde su versión 3.1+, en Internet Explorer 8 y en Safari 3.1+. Pero hasta que no borremos del mapa a IE6 e IE7 tendremos que seguir utilizando herramientas como jQuery.

Web Storage

Es un nuevo mecanismo de almacenamiento local que, a diferencia de las Cookies, permite almacenar un gran volumen de datos (Por ejemplo, los emails de una cuenta, haciendo innecesario plugins como Google Gears), y también permite almacenarlo accesible a todas las ventanas del dominio al que pertenecen.

Todo se realizaría de un modo muy sencillo: Simplemente accediendo al atributo que proceda dentro del objeto localStorage.

De nuevo, está disponible ya en Firefox 3.5, IE8, Safari 4.0, Chromium…

Web Workers

Permite lanzar distintos hilos de ejecución que se ejecutan en paralelo a la página principal. De este modo, se puede trabjar en un entorno thread-like trabajando con un mecanismo de coordinación de pase de mensajes.

Como ejemplos, podríamos:

  • Generar una hebra que busque números primos en background.
  • Un worker que actualice una base de datos local. Estaría permanentemente escuchando al servidor mediante un WebSocket, y cuando hubiera algún cambio, modificaría la base de datos local.
  • Operaciones de Lectura/Escritura de fondo.
  • Workers compartido. Distintas ventanas podría compartir un único Worker que, en un momento dado, podría actualizar a todas a la vez.
  • Delegación. Aprovechando las distintas CPUs de los microprocesadores multi-core.

Lógicamente, esto abre un gran número de posibilidades en cuanto al rendimiento de las páginas web.

Por el momento disponible en Firefox 3.5+, Safari 4, Chromium.

Server-Sent Events

La API que define esta especificación puede no parecer muy revolucionaria: Permite recivir notificaciones PUSH desde el servidor en forma de evento DOM. Perfectamente podemos lograr el mismo objetivo mediante XHR o un iframe. Sin embargo, dada su naturaleza, permitiria que, cuando el navegador se pueda coordinar con el operador de red, se ahorraran notablemente recursos de red. Uno de los efectos colaterales sería aumentar la duración de las baterías de los dispositivos portátiles.

WebSockets API

Esta API permite una auténtica conexión bidireccional entre el navegador y el servidor. Ya fue cubierta en este blog cuando Google Chrome anunció su implementación.

Web SQL Database

Es exactamente lo que su nombre sugiere. Consiste en una especificación de un API que permitirá almacenar información en una base de datos locale accesible mediante SQL.

Veamos un ejemplo de su funcionamiento:

function prepareDatabase(ready, error) {
  return openDatabase('documents', '1.0', 'Offline document storage', 5*1024*1024, function (db) {
    db.changeVersion('', '1.0', function (t) {
      t.executeSql('CREATE TABLE docids (id, name)');
    }, error);
  });
}

function showDocCount(db, span) {
  db.readTransaction(function (t) {
    t.executeSql('SELECT COUNT(*) AS c FROM docids', [], function (t, r) {
      span.textContent = r.rows[0].c;
    }, function (t, e) {
      // couldn't read database
      span.textContent = '(unknown: ' + e.message + ')';
    });
  });
}

prepareDatabase(function(db) {
  // got database
  var span = document.getElementById('doc-count');
  showDocCount(db, span);
}, function (e) {
  // error getting database
  alert(e.message);
});

En primer lugar se define la función prepareDatabase() que, en caso de que fuera necesario, crea la base de datos con una tabla llamada “docids” con dos columnas (“id” y “name”). Si hay éxito, o no es necesario crearla, se llama a la función getDatabase(), que obtiene un manejador de la base de datos, y entonces llama a la función que hace realmente el trabajo, en este caso showDocCount().

File API

Como último ejemplo de apartados en los que trabaja el WebApps Working Group, voy a mosotrar la File API. Permitirá un manejo avanzado de ficheros desde JavaScript, pudiendo:

  • Una vez que el usuario conceda permisos, el navegador permitirá leer y parsear un fichero.
  • Se podrá almacenar información de forma local.
  • Se permitirá guardar los archivos, del mismo modo que ahora se pueden descargar ficheros de servidores remotos.
  • Se podrán enviar ficheros grandes a servidores de un modo más eficiente que el actual. Por ejemplo, se podrá dividir en porciones.
  • Los navegadores implementarán mecanismos para poder cancelar o impedir los casos de uso enumerados anteriormente.

Sin duda, APIs como éstas permitirán realizar aplicaciones reales que funcionen en el navegador sin necesidad de un servidor. Como ejemplo, un reproductor multimedia avanzado, que guarde una caché local con metadatos de nuestra música y películas.

Creo que muy pronto comenzaremos a ver todas estas nuevas aplicaciones ir aparenciendo, poco a poco, hasta que un día el navegador realmente sea un framework de aplicaciones más.

Vídeo, audio e imágenes de entrada al navegador

Hace poco se ha publicado un nuevo borrador del w3c (The Capture API) que permitirá conectar dispositivos de entrada al navegador, permitiendo por ejemplo capturar una fotografía, mensaje de audio por el micrófono, o un vídeo.

Aun se encuentra en un estado muy prematuro, pero nos enumeran una serie de posibles casos de uso:

  • Captura y envío de imágenes. Permitiendo el envío por XHR de varias imágenes.
  • Captura de imagen panorámica. Por ejemplo, con 3 tomas que posteriormente se “concatenan” automáticamente para mostrar la panorámica.
  • Video Chat. Aun está sin definir el método para realizar streaming, sin embargo todas las soluciones propuestas pasan por websockets.
  • Cámara web. Permitirá realizar una aplicacion de vigilancia para controlar la cámara, incluyendo movimientos como arriba, abajo, izquierda o derecha, o hasta aplicaciones de detección de movimiento.
  • Búsqueda por voz. Introduciendo por el micrófono la cadena de búsqueda.
  • Recordatorio de voz. De nuevo, desde el micrófono.

Por supuesto, para proteger la privacidad de los usuarios, el navegador tendrá que pedir explícitamente confirmación del usuario para permitir que se acceda a estos dispositivos.

Ejemplo de uso:

// Create a container div element and append it to the document body.
var container = document.createElement("div");
document.body.appendChild(container);

// The browser viewport width in pixels.
var screenWidth = window.innerWidth;

function successCallback(data) {
for (var i in data) {
var img = document.createElement("img");
img.src = data[i].uri;
// If the image width exceeds that of the browser viewport, the image
// is scaled to fit the screen keeping the aspect ratio intact.
if (data[i].format.width > screenWidth) {
img.style.width = screenWidth + "px";
img.style.height = (data[i].format.height/data[i].format.width)*screenWidth + "px";
}
container.appendChild(img);
}
}

function errorCallback(err) {
alert(err.message + " (" + err.code + ")");
}

// Launch the device camera application and invoke the callback once
// the user exits the camera application.
transactionId = navigator.device.captureImage(successCallback, 1, errorCallback);

Mientras tanto, y hasta que esto sea una realidad, se puede utilizar el paquete flash.media de ActionScript3, en concreto las clases Camera y Micrphone. Pero claro, no es lo mismo. Una verdadera lástima tener que esperar tanto tiempo. Veremos cuándo tiene el navegador más utilizado estas funcionalidades.