#247 Offline Apps Part 1
- Download:
- source codeProject Files in Zip (112 KB)
- mp4Full Size H.264 Video (19.4 MB)
- m4vSmaller H.264 Video (12.6 MB)
- webmFull Size VP8 Video (25.4 MB)
- ogvFull Size Theora Video (24.6 MB)
En este episodio vamos a trabajar con una aplicación Rails sencillita llamada “Lista de la Compra”, que almacena una serie de elementos. Para usarla tan sólo tenemos que teclear el nombre del ítem y pulsar en el botón “Add Item”. A continuación la página se recargará y mostrará el elemento recién añadido en la lista.
Si bien la aplicación funciona correctamente cuando estamos conectados a la red, si la utilizamos en un teléfono móvil no siempre tendremos cobertura y por tanto no podremos acceder a la lista de la compra. Sería útil que la aplicación funcionase sin conexión, y esto es lo que aprenderemos en este episodio.
Introducción al manifiesto de caché
Demostraremos nuestra aplicación en un navegador de escritorio pero por lo general sólo querremos que esté disponible sin conexión la versión móvil del sitio. Ya vimos en el episodio 199 [verlo, leerlo] cómo hacer una versión específica para dispositivos móviles. Si seguimos este enfoque conseguiremos que la versión móvil sea más sencilla, y eso hará que sea más fácil hacer que esté disponible sin conexión.
Para que la aplicación funcione sin conexión a la red utilizaremos una técnica de HTML5 basada en el manifiesto de caché. El sitio Dive Into HTML5, que es una de las mejores fuentes de información sobre HTML5, tiene una página dedicada a las aplicaciones sin conexión (en inglés) que merece la pena leer para ponernos al día en este tema.
Lo primero es crear un manifiesto de caché, y existe una gema llamada rack-offline que nos puede ser de ayuda. La forma más fácil de usarla es añadir la siguiente ruta a nuestra aplicación.
match "/application.manifest" => Rails::Offline
La ruta apunta a una aplicación Rack llamada Rails::Offline
. Si se visita /application.manifest
se generará el manifiesto de caché con ciertos valores por defecto. Por supuesto tenemos que añadir una referencia a esta gema en el fichero Gemfile
de nuestra aplicación, y luego ejecutar la orden bundle
para asegurar su instalación.
/Gemfile
gem 'rack-offline'
A continuación añadiremos la ruta a application.manifest
.
match "/application.manifest" => Rails::Offline
Si visitamos con el navegador http://localhost:3000/application.manifest
veremos que nos devuelve un fichero de manifiesto de caché. Dicho fichero contiene una lista de los archivos que deben ser descargados para que la aplicación pueda funcionar sin conexión. La gema rack-offline añadirá, por defecto, todos los archivos HTML, CSS y JavaScript del directorio /public
. Esto sólo funcionará con aplicaciones pequeñas pero se puede personalizar para elegir los archivos que se tendrán que incluir en el manifiesto, se describen los detalles para hacerlo en la documentacion de rack-offline.
Nótese que hay una suma de control al comienzo del manifiesto. Con esto Rack-offline puede identificar revisiones específicas del manifiesto de caché. Mientras la aplicación esté en modo de desarrollo la suma cambiará cada vez que carguemos la página, pero en producción sólo cambiará cuando uno de los archivos listados en el manifiesto cambie lo que indica al navegador que debe descargar otra vez los archivos.
Aunque ya tenemos un manifiesto de caché todavía tenemos que decirle a la aplicación que lo utilice. Esto se hace poniendo el atributo manifest
a la etiqueta HTML del fichero de layout de la aplicación.
<html manifest="/application.manifest">
El atributo manifest
hace que se descarguen los archivos enumerados en el manifiesto de caché cada vez que alguien cargue una página de esta aplicación, así como la propia página de forma que esté disponible cuando no exista conexión.
Si ahora visitamos la página del listado de nuestra aplicación se descargarán todos los ficheros anotados en el manifiesto, así como la propia página que contiene el listado. Esto se puede comprobar visitando la página y luego parando el servidor Rails de la aplicación. Si después abrimos una ventana del navegador y visitamos http://localhost:3000/items
veremos que la página sigue ahí incluso con el servidor parado, porque está siendo servida desde la caché.
Pero todavía tenemos un problema. Si recargamos la página veremos que se pierde la hoja de estilos.
De hecho el navegador no está logrando cargar las hojas de estilos ni ninguno de los ficheros JavaScript de la aplicación. El motivo son los sellos de tiempo que Rails añade a la URL de estos archivos.
Estos sellos hacen que el navegador considere que los archivos son distintos de los que han sido declarados en el manifiesto de caché, y por tanto los intenta cargar de nuevo del servidor. Se puede solucionar este problema configurando una variable de entorno que elimina los sellos de tiempo en nuestra aplicación añadiendo la siguiente línea debajo de /config/application.rb
.
ENV["RAILS_ASSET_ID"] = ""
Para que esto funcione tenemos que volver a arrancar el servidor y visitar otra vez la página, recargándola una o dos veces para asegurarnos de que todo ha sido cargado y cacheado correctamente. Una vez hecho esto podremos parar de nuevo el servidor y veremos que la aplicación funcionará correctamente sin conexión, cargándose correctamente los ficheros CSS y JavaScript, dado que no tendrán sellos de tiempo tras los nombres.
Problemas con la caché
Durante lo que queda de episodio tendremos el servidor arrancado para simular que estamos navegando con conexión y así ver algunos problemas potenciales. Lo primero de lo que nos daremos cuenta es de que los cambios que hacemos en nuestra aplicación no surten efecto inmediatamente. Por ejemplo, supongamos que queremos cambiar el texto en el botón “Add Item” para que sea “Add”. Se trata de un cambio fácil, tan sólo tenemos que cambiar el texto en el código de la vista.
<%= form_for Item.new do |f| %> <%= f.text_field :name %> <%= f.submit "Add" %> <% end %>
Al guardar este archivo y recargar la página esperaríamos que el texto del botón cambiase, pero no lo hace. El motivo es que el navegador devuelve la versión cacheada de la página: no pregunta si el servidor está levantado sino que devuelve de inmediato lo que tiene en caché. Sin embargo en segundo plano comprobará el manifiesto de caché para ver si ha habido cambios y entonces sí que descargará los ficheros modificados. Si luego volvemos a cargar la página por segunda vez veremos que el texto del botón sí que cambia porque para entonces el navegador ya habrá detectado los cambios y los habrá cacheado.
Esto quiere decir que para que los cambios hagan efecto en desarrollo debemos acostumbrarnos a recargar la página un par de veces cada vez que hagamos un cambio.
Se puede presentar otro problema cuando la caché apunte a un fichero que ya no existe o que tiene otro nombre. Por ejemplo si eliminásemos el archivo 422.html
del directorio /public/
el manifiesto dejaría de servir. Para demostrarlo, cambiemos otra vez el texto del botón para que sea “Add Another Item”.
<%= form_for Item.new do |f| %> <%= f.text_field :name %> <%= f.submit "Add Another Item" %> <% end %>
Ya podemos recargar la página todas las veces que queramos que el texto del botón no cambiará, porque el manifiesto aborta al llegar al fichero 422.html
que hemos eliminado, por lo que nunca actualiza la caché de la página de lista de la compra. No veremos los cambios nunca incluso aunque el servidor continúe activo.
Es difícil depurar este tipo de problemas porque no hay ningún síntoma de que se ha producido este error con el manifiesto de caché. Para ayudarnos con esto podemos añadir código JavaScript que esté pendiente del evento de error en el objeto applicationCache
y detecte cuando se produzcan estos errores.
Como en la aplicación estamos usando jQuery podemos añadir esta detección de errores con el siguiente código en nuestro fichero application.js
.
$(function () { $(windows.applicationCache.bind('error', function () { alert('There was an error when loading the cache manifest.'); })) })
Esta alerta no funcionará con la aplicación en el estado en que está porque al tener el manifiesto roto no recargará el nuevo fichero application.js
pero la alerta de JavaScript saltará la próxima vez que tengamos un problema parecido, y así sabremos que hay algo mal.
Para arreglar ahora el manifiesto de caché tan sólo tenemos que reiniciar el servidor web para que el fichero del manifiesto se actualice automáticamente. Con esto volverá el comportamiento anterior de la aplicación y cuando hagamos cambios siempre los veremos tras recargar un par de veces la página.
La caché de contenidos puede presentar problemas que sólo detectaremos en producción. Si añadimos un elemento a la lista de la compra el nuevo elemento será añadido a la base de datos. La página de la lista se actualiza dinámicamente desde la base de datos y veremos que aparecen todos los elementos. En modo de desarrollo todo parece funcionar: el elemento se añade a la lista y sigue ahí cuando recargamos la página.
Pero ¿y si intentamos hacer esto en producción? Para simularlo haremos que cache_classes
sea true
en development.rb
(si usamos las técnicas de este capítulo en nuestras aplicaciones tendremos que probar exhaustivamente en modo de producción)
config.cache_classes = true
Tenemos que reiniciar el servidor para que coja este cambio. Una vez que lo tengamos, añadiremos otro elemento a la lista, por ejemplo “chunky bacon”.
El elemento que acabamos de añadir aparece porque acabamos de hacer una petición POST pero desaparecerá si recargamos la página: se vuelve a mostrar la versión antigua cacheada de la página, nuevamente por culpa del manifiesto de caché. Ahora que tenemos la aplicación en producción el manifiesto no cambia, por lo que el navegador desconoce que la página ha sido modificada y vuelve a servir la versión antigua. Este problema sólo ocurre en producción, no en desarrollo.
La cuestión clave es: ¿cómo controlamos los contenidos dinámicos? Es un tema más complejo de lo que podría parecer en un principio, el servidor no puede actualizar dinámicamente la página porque se encuentra cacheada por el navegador. Lo que podemos hacer es actualizar su contenido por JavaScript, y esto es lo que haremos en el siguiente episodio.