Capítulo 9 Sin conexión

Si bien todos los navegadores tienen mecanismos de almacenamiento en caché, estos sistemas no son fiables y no siempre funcionan como debieran. HTML5 permite resolver algunas de las molestias asociadas al trabajo sin conexión mediante la interfaz ApplicationCache.

Algunas de las ventajas que conlleva el uso de ésta caché para una aplicación son:

  • Navegación sin conexión: los usuarios pueden explorar todo el sitio web sin conexión.
  • Velocidad: los recursos almacenados en caché son locales y, por tanto, se cargan más rápido.
  • Reducción de carga del servidor: el navegador solo descarga recursos del servidor que han cambiado.

Para poder trabajar sin conexión, una aplicación únicamente necesita de un archivo de manifiesto, el cual indica al navegador que ficheros debe almacenar en la caché local. El contenido del manifiesto puede ser tan simple como un listado de archivos. Una vez que el navegador ha descargado y almacenado los ficheros (html, CSS, imágenes, javascripts, etc), el navegador hace uso de estos ficheros, incluso cuando el usuario actualiza la página en su navegador.

Además de especificar qué ficheros van a ser almacenados en la caché, es posible indicar cuáles no tienen que serlo, y por tanto obligar al navegador a realizar una petición de dichos ficheros al servidor. Finalmente, si intentamos acceder a un fichero no almacenado en local, y no disponemos de conexión, podemos mostrar un recurso que previamente hemos almacenado en la caché.

9.1 El archivo de manifiesto de caché

El archivo de manifiesto es lo que le indica al navegador cuando y qué tiene que almacenar en su caché, y qué tiene que traerse de la Web. Indicar al navegador el manifiesto que tiene que utilizar es muy sencillo:

<!DOCTYPE html>
<html lang="en" manifest="/example.appcache">
    ...
</html>

El atributo manifest debe estar incluido en todas las páginas de nuestra aplicación, que queramos que se almacenen en la caché. Es decir, además de los ficheros indicados en el manifiesto, la propia página que incluye el manifiesto es almacenada en la caché. El navegador no almacenará en caché ninguna página que no contenga el atributo manifest (a menos que esa página aparezca explícitamente en el propio archivo de manifiesto).

El atributo manifest puede señalar a una URL absoluta o a una ruta relativa, pero las URL absolutas deben tener el mismo origen que la aplicación web. Un archivo de manifiesto puede tener cualquier extensión, pero se debe mostrar con el tipo MIME correcto:

<html manifest="http://www.example.com/example.mf">
    ...
</html>

El tipo MIME con el que se deben mostrar los archivos de manifiesto es text/cache-manifest. Es posible que se tenga que añadir un tipo de archivo personalizado a la configuración de .htaccess o de tu servidor web.

9.1.1 Estructura

Ejemplo de un archivo de manifiesto sencillo:

CACHE MANIFEST
index.html
stylesheet.css
images/logo.png
scripts/main.js

El archivo de manifiesto del ejemplo permite almacenar en caché los cuatro archivos especificados. El formato del manifiesto es importante:

  • La cadena CACHE MANIFEST debe aparecer en la primera línea y es obligatoria.
  • Dentro del manifiesto, los ficheros son listados dentro de categorías, también conocidos como namespaces. Si no se especifica ninguna categoría, todos los ficheros pertenecen a la categoría CACHE.

Un ejemplo más complejo sería:

CACHE MANIFEST
# 2010-06-18:v2
 
CACHE:
/favicon.ico
index.html
stylesheet.css
images/logo.png
scripts/main.js
 
# Resources that require the user to be online.
NETWORK:
login.php
/myapi
http://api.twitter.com
 
# static.html will be served if main.py is inaccessible
# offline.jpg will be served in place of all images in images/large/
# offline.html will be served in place of all other .html files
FALLBACK:
/main.py /static.html
images/large/ images/offline.jpg
*.html /offline.html

Un archivo de manifiesto puede incluir tres categorías: CACHE, NETWORK y FALLBACK.

  • CACHE: esta es la sección predeterminada para las entradas. Los archivos incluidos en esta sección (o inmediatamente después de CACHE MANIFEST) se almacenarán en caché explícitamente después de descargarse por primera vez.
  • NETWORK: los archivos incluidos en esta sección son recursos permitidos que requieren conexión al servidor. En todas las solicitudes enviadas a estos recursos se omite la caché, incluso si el usuario está trabajando sin conexión. Se pueden utilizar caracteres comodín.
  • FALLBACK: se trata de una sección opcional en la que se especifican páginas alternativas en caso de no poder acceder a un recurso. La primera URI corresponde al recurso y la segunda, a la página alternativa. Ambas URI deben estar relacionadas y tener el mismo origen que el archivo de manifiesto. Se pueden utilizar caracteres comodín.
CACHE MANIFEST
# 2010-06-18:v3
 
# Explicitly cached entries
index.html
css/style.css
 
# offline.html will be displayed if the user is offline
FALLBACK:
/ /offline.html
 
# All other resources require the user to be online.
NETWORK:
*
 
# Additional resources to cache
CACHE:
images/logo1.png
images/logo2.png
images/logo3.png

Nota

Las peticiones de recursos que den como resultado un error 404 (por ejemplo una imagen no encontrada), mostrarán en este caso el fichero offline.html

9.2 Cómo servir el manifiesto

Como hemos comentado anteriormente, el manifiesto puede tener cualquier extensión (aunque se recomienda que sea .appcache), pero lo importante es que el servidor envíe el fichero con el tipo MIME correcto. Si utilizamos Apache, es tan sencillo como añadir la siguiente línea al fichero mime.types:

text/cache-manifest appcache

Esta configuración dependerá del servidor web que utilicemos. De todas maneras, para asegurarnos que el servidor está enviando el manifiesto con la cabecera correcta, podemos utilizar una herramienta como curl de la siguiente manera:

curl -I http://mysite.com/manifest.appcache

O bien a través de las herramientas de desarrollo integradas en Google Chrome, Safari y Firefox. De cualquiera de las maneras, la respuesta tendría que ser algo parecido a esto:

HTTP/1.1 200 OK
Date: Mon, 13 Sep 2010 12:59:30 GMT
Server: Apache/2.2.13 (Unix) mod_ssl/2.2.13 OpenSSL/0.9.8l DAV/2 PHP/5.3.0
Last-Modified: Tue, 31 Aug 2010 03:11:00 GMT
Accept-Ranges: bytes
Content-Length: 113
 
Content-Type: text/cache-manifest

9.3 Proceso de cacheado

Cuando visitamos una página que hace uso de la cache de aplicación, el proceso de cacheado que se sigue es el siguiente:

  1. Navegador: solicita la página http://html5app.com/
  2. Servidor: devuelve index.html
  3. Navegador: procesa la página index.html y solicita los recursos asociados, como imágenes, javascripts, hojas de estilos y el manifiesto.
  4. Servidor: devuelve todos los recursos solicitados.
  5. Navegador: procesa el manifiesto y solicita, de nuevo, todos los recursos definidos en el manifiesto. Efectivamente, se produce una doble petición.
  6. Servidor: devuelve todos los recursos del manifiesto solicitados.
  7. Navegador: la cache de aplicación está actualizada, y se lanzan los eventos asociados.

Ahora, el navegador está listo y la caché contiene los ficheros indicados en el manifiesto. Si el manifiesto no ha cambiado, y la página se recarga, ocurre lo siguiente:

  1. Navegador: vuelve a solicitar la página http://html5app.com/
  2. Navegador: detecta que tiene una copia local de index.html y la sirve de manera local.
  3. Navegador: procesa la página index.html y los recursos existentes en la caché se sirven de manera local.
  4. Navegador: solicita de nuevo el manifiesto al servidor.
  5. Servidor: devuelve un código 304 indicando que no ha cambiado nada en el manifiesto.

Una vez que el navegador tiene almacenados los recursos en su caché, los sirve de manera local y después solicita el manifiesto. Como se puede ver en la siguiente captura de pantalla, Google Chrome únicamente solicita al servidor aquellos ficheros que no se encuentran en la caché de la aplicación.

Peticiones realizadas por Google Chrome

Figura 9.1 Peticiones realizadas por Google Chrome

Si deseamos actualizar alguno de los recursos de la aplicación, tendremos que actualizar primero el manifiesto, para obligar al navegador a solicitar de nuevo todos los recursos. Para ello, es necesario marcar de alguna manera que el manifiesto ha cambiado, aunque los ficheros a cachear sean los mismos. Una práctica muy sencilla es añadir una número de versión o la fecha de modificación del manifiesto:

# 2010-06-18:v3

Una vez que el manifiesto ha cambiado, el comportamiento del navegador es el siguiente:

  1. Navegador: vuelve a solicitar la página http://html5app.com/
  2. Navegador: detecta que tiene una copia local de index.html y la sirve de manera local.
  3. Navegador: procesa la página index.html y los recursos existentes en la caché se sirven de manera local.
  4. Navegador: solicita de nuevo el manifiesto al servidor.
  5. Servidor: devuelve el nuevo manifiesto modificado.
  6. Navegador: procesa el manifiesto y solicita todos los recursos definidos en el manifiesto.
  7. Servidor: devuelve todos los recursos del manifiesto solicitados.
  8. Navegador: la cache de aplicación está actualizada, y se lanzan los eventos asociados.

Hay que destacar, que a pesar de haber modificado los recursos en el navegador, estos cambios no se producen en este momento, ya que se siguen utilizando los cargados previamente. La nueva caché solo estaría disponible si volviésemos a recargar la página. Una manera de modificar este comportamiento es accediendo al objeto applicationCache.

9.4 Actualización de la memoria caché

El objeto window.applicationCache permite acceder mediante JavaScript a la caché de aplicación del navegador. Su propiedad status permite comprobar el estado de la memoria caché, y es el encargado de notificarnos que se ha producido un cambio en la caché local.

var appCache = window.applicationCache;
switch (appCache.status) {
    case appCache.UNCACHED: // UNCACHED == 0
        return 'UNCACHED'; break;
    case appCache.IDLE: // IDLE == 1
        return 'IDLE'; break;
    case appCache.CHECKING: // CHECKING == 2
        return 'CHECKING'; break;
    case appCache.DOWNLOADING: // DOWNLOADING == 3
        return 'DOWNLOADING'; break;
    case appCache.UPDATEREADY:  // UPDATEREADY == 4
        return 'UPDATEREADY'; break;
    case appCache.OBSOLETE: // OBSOLETE == 5
        return 'OBSOLETE'; break;
    default:
        return 'UKNOWN CACHE STATUS'; break;
};

Para actualizar la caché mediante JavaScript, primero se debe hacer una llamada a applicationCache.update(). Al hacer esa llamada, se intentará actualizar la caché del usuario (para lo cual será necesario que haya cambiado el archivo de manifiesto). Finalmente, cuando el estado de applicationCache.status sea UPDATEREADY, al llamar a applicationCache.swapCache(), se sustituirá la antigua caché por la nueva.

var appCache = window.applicationCache;
 
appCache.update(); // Attempt to update the user's cache.
 
if (appCache.status == window.applicationCache.UPDATEREADY) {
    appCache.swapCache();  // The fetch was successful, swap in the new cache.
}

Al utilizar update() y swapCache() de este modo, no se muestran los recursos actualizados a los usuarios. El flujo indicado solo sirve para pedirle al navegador que busque un nuevo archivo de manifiesto, que descargue el contenido actualizado que se especifica y que actualice la caché de la aplicación. Por tanto, la página se tiene que volver a cargar dos veces para que se muestre el nuevo contenido a los usuarios: una vez para extraer una nueva caché de aplicación y otra para actualizar el contenido de la página.

Para que los usuarios puedan acceder a la versión más reciente del contenido de tu sitio, podemos establecer un escuchador que controle el evento updateready cuando se cargue la página:

window.addEventListener('load', function(e) {
    window.applicationCache.addEventListener('updateready', function(e) {
        if (window.applicationCache.status == window.applicationCache.UPDATEREADY) {
            // Browser downloaded a new app cache.
            // Swap it in and reload the page to get the new hotness.
            window.applicationCache.swapCache();
            if (confirm('A new version of this site is available. Load it?')) {
                window.location.reload();
            }
        } else {
            // Manifest didn't changed. Nothing new to server.
        }
    }, false);
}, false);

Hay, además, algunos eventos adicionales que permiten controlar el estado de la caché. El navegador activa eventos para una serie de acciones (como el progreso de las descargas, la actualización de la caché de las aplicaciones y los estados de error). El siguiente fragmento permite establecer escuchadores de eventos para cada tipo de evento de caché:

function handleCacheEvent(e) {}
 
function handleCacheError(e) {
  alert('Error: Cache failed to update!');
};
 
// Fired after the first cache of the manifest.
appCache.addEventListener('cached', handleCacheEvent, false);
 
// Checking for an update. Always the first event fired in the sequence.
appCache.addEventListener('checking', handleCacheEvent, false);
 
// An update was found. The browser is fetching resources.
appCache.addEventListener('downloading', handleCacheEvent, false);
 
// The manifest returns 404 or 410, the download failed,
// or the manifest changed while the download was in progress.
appCache.addEventListener('error', handleCacheError, false);
 
// Fired after the first download of the manifest.
appCache.addEventListener('noupdate', handleCacheEvent, false);
 
// Fired if the manifest file returns a 404 or 410.
// This results in the application cache being deleted.
appCache.addEventListener('obsolete', handleCacheEvent, false);
 
// Fired for each resource listed in the manifest as it is being fetched.
appCache.addEventListener('progress', handleCacheEvent, false);
 
// Fired when the manifest resources have been newly redownloaded.
appCache.addEventListener('updateready', handleCacheEvent, false);

Si no se puede descargar el archivo de manifiesto o algún recurso especificado en él, fallará todo el proceso de actualización. Si se produce ese fallo, el navegador seguirá utilizando la antigua caché de la aplicación.

9.5 Eventos online/offline

Como parte de la especificación de HTML5, el objeto navigator incluye una propiedad que nos indica si se dispone de conexión o no, concretamente navigator.onLine. Sin embargo, esta propiedad no se comporta de manera correcta en la mayoría de navegadores, y únicamente cambia su estado al indicar de manera explícita que funcione en modo offline. Como desarrolladores, lo que realmente nos interesa es conocer si realmente hay conexión o no con el servidor.

Una manera de identificar si existe conexión a internet, es utilizar la categoría FALLBACK del manifiesto. En esta categoría podemos indicar dos ficheros JavaScript que detectan si estamos online o no:

CACHE MANIFEST
 
FALLBACK:
online.js offline.js

online.js contiene:

setOnline(true);

Y offline.js contiene:

setOnline(false);

En nuestra aplicación, creamos una función llamada testOnline que dinámicamente crea un elemento <script>, el cual trata de cargar el fichero online.js. Si la carga se realiza de manera correcta, se ejecuta el método setOnline(true). Si estamos offline, el navegador cargará el fichero offline.js, ejecutando el método setOnline(false).

function testOnline(fn) {
    var script = document.createElement(‘script’)
    script.src = 'online.js';
    // alias the setOnline function to the new function that was passed in
    window.setOnline = function (online) {
        document.body.removeChild(script);
        fn(online);
    };
 
    // attaching script node trigger the code to run
    document.body.appendChild(script);
}
 
testOnline(function (online) {
    if (online) {
        applicationCache.update();
    } else {
        // show users an unobtrusive message that they're disconnected
    }
});

Ejercicio 11

Ver enunciado