13 sept 2011

GWT: incluir librerías JavaScript externas


Si bien GWT nos proporciona una cantidad muy interesante de métodos para manipular el DOM directamente desde Java, hay ocasiones que es mucho más práctico incluir JavaScript directamente a través de JSNI, si bien no es aconsejable ya que el código incluido no es verificado ni optimizado por el compilador.
Por otra parte, hay ahí fuera miles de librerías y utilidades súper prácticas que es imposible pasar por alto, algunas insustituibles más teniendo en cuenta que GWT tiene serias carencias en algunas áreas, los efectos son un ejemplo, salvo un manojo de buenas herramientas portadas a Java el abanico es escaso, además no siempre tenemos un equivalente a la altura, varias veces los ports terminan siendo más voluminosos que la solución original y a veces con menos posibilidades.
Todo esto nos lleva a más que escribir unas pocas líneas a través de JSNI vernos tentados a incluir librerías externas completas para tener funcionalidades extra en nuestra aplicación, el tema es como lo hacemos.

Lo más básico es escribir un tag link en nuestro archivo home de HTML referenciando el código externo, opcion esta desaconsejada, no solo perdemos el control ya que confiamos en un recurso externo que no sabemos si invariablemente va a estar cuando hagamos el despliegue (con GWT también se hacen pequeños módulos que podemos distribuir independientemente, no solo apps completas), si no que delegamos en el browser las tareas de control del cache y manejo de errores, estamos jugando con fuego se podría decir.
Lo otro es otra incluirlo en nuestro archivo XML de proyecto utilizando la Automatic Resource Inclusion para incluir código externo de una manera más elaborada, pero en este caso siempre lo incluirá más allá de que finalmente lo necesitemos o no.

Precisamente viendo como podemos hacer todo esto de una manera un poco más elegante es que llegamos a la dimensión desconocida de la interfaz Client Bundle para el manejo de recursos externos. Una de las ventajas que ya nos mencionan es que los recursos incluidos a través de ésta pasan a la categoría de recursos cacheados para siempre (repito la importancia  de tener el control de que se cachea y como), lo cual es muy conveniente para recursos que no cambian con el tiempo, los más obvios y por tanto conocidos son imágenes y archivos CSS. Estos cuentan con soporte exaustivo, desde los prácticos sprites de imágenes a cosas más rebuscadas como soporte RTL, pero... y los archivos JS?
En el caso de archivos JavaScript la cosa es un poco acotada, de los tipos disponibles utilizamos el TextResource, que engloba los recursos de "static text content", una linda manera de referirse al JS de toda la vida. 
Otra cosa muy bienvenida que tenemos desde la release 2.4 es la clase ScriptInjector que nos permite crear un tag SCRIPT sin tener que manejar directamente el DOM lo cual es propenso a errores (por ejemplo si nos salteáramos un paso como incluir el tag sin definir el tipo o similares puede fallar), incluso tenemos algunos métodos extra como definir un setCallback en caso de archivos cargados desde una URL, básico pero nada mal.

Comencemos con un ejemplo así vemos como usar esta técnica que seguro nos ayuda a perder el miedo a incluir recursos externos o hacerlo de una manera más consistente, en este caso estaba tratando de utilizar los features de HTML5 Storage, pero en caso de que esta funcionalidad no estuvieran disponible en el cliente quería ofrecer un método alternativo de almacenar datos en el browser, si bien hay excelentes librerías que chequean exhaustivamente el navegador para ver que tenemos disponible (desde almacenamiento Flash hasta características exclusivas de IE entre otras) me decidí por lo simple y utilicé una vieja amiga como es TaffyDB, una muy buena opción para almacenar info en el cliente con algunos métodos muy útiles: contar, ordenar, filtros y algunas cosillas más.


estructura del ejemplo


Allá vamos entonces, proyecto nuevo en Eclipse y creamos un directorio para almacenar Taffy, en este caso bajo el package client/lib, utilizamos la versión minificada de producción taffy-min.js, menos de 20kb... un lujo.
Ahora lo interesante, para incluirla debemos definir una interface que extienda de ClientBundle y tenga la ruta a la librería como argumento de la anotación @Source y un método para referenciarla por el tipo, en mi caso la llame TaffyDBBundle quedando de la siguiente manera:

package testTaffy.client;

import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.TextResource;

public interface TaffyDBBundle extends ClientBundle {
 @Source("lib/taffy-min.js")
 TextResource taffy();
}

Es bastante descriptiva, como mencioné especificamos la ruta al archivo en @Source y definimos un método para accederla del tipo TextResource, en mi caso taffy();
Ahora en el onModuleLoad vamos integrar todo usando el ScriptInjector, aquí tenemos dos opciones: ScriptInjector.fromString y ScriptInjector.fromUrl (no confundir el tipo de retorno con el nombre del método) en este caso como ya tenemos incluida la librería utilizaremos fromString, que recibe como argumento el contenido de nuestro archivo JS, a este lo accedemos mediante el método getText() de la interfaz TextResource que ya implementamos, quedando así:

package test.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.ScriptInjector;

public class TestDB implements EntryPoint {

 public void onModuleLoad() {

  TaffyDBBundle taffy = GWT.create(TaffyDBBundle.class);
  JavaScriptObject result = ScriptInjector.fromString(taffy.taffy().getText()).inject();
  
 }
 
}

Primero creamos nuestro recurso a través de GWT.create quedando del tipo de nuestra interfaz (TaffyDBBundle), luego utilizamos el ScriptInjector.fromString y le pasamos como argumento el llamado a nuestra instancia mediante el método taffy() (podemos llamarlo como queramos) y luego su contenido mediante .getText(), finalmente incluimos el script al DOM mediante .inject();
Para verificar vamos a crear un par de métodos mediante JSNI para verificar que está funcionando, en este caso definimos una base de datos e ingresamos algunos registros, luego verificamos consultando cuantos cumplen con determinada condición:

    private native void createDB() /*-{

  $wnd.sobrinos = TAFFY([ {"id":1,"user":"Hugo","edad":"12"},
        {"id":2,"user":"Paco","edad":"12"},
        {"id":3,"user":"Luis","edad":"12"}  
                        ]);

 }-*/;

 private native void testDB() /*-{

  alert($wnd.sobrinos({"edad":"12"}).count());

 }-*/;


vale recordar que en GWT siempre es conveniente referirse a las variable a través de el objeto $wnd (window) para evitar problemas, ahora juntamos todo:
package testTaffy.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.ScriptInjector;

/**
 * Entry point classes define onModuleLoad().
 */
public class TestTaffyDB implements EntryPoint {

 public void onModuleLoad() {

  TaffyDBBundle taffy = GWT.create(TaffyDBBundle.class);
  JavaScriptObject result = ScriptInjector.fromString(taffy.taffy().getText()).inject();
  
  if (null != result) {
   createDB();
   testDB();
  }
 }

 private native void createDB() /*-{

  $wnd.sobrinos = TAFFY([ {"id":1,"user":"Hugo","edad":"12"},
        {"id":2,"user":"Paco","edad":"12"},
        {"id":3,"user":"Luis","edad":"12"}  
                        ]);

 }-*/;

 private native void testDB() /*-{

  alert($wnd.sobrinos({"edad":"12"}).count());

 }-*/;
}


Utilizo un JavaScriptObject result para verificar que el tag se creo correctamente, al correrlo obtenemos un mensaje de alerta con 3, efectivamente está funcionando ya que hay 3 usuarios cargados en Taffy con el campo edad igual a 12, bastante accesible, como mencionaba hay más métodos útiles disponibles como setCallback al cargar recursos por URL o setWindow para especificar en que ventana queremos colocar el tag SCRIPT, simple y consistente.

Por último juro que busqué que edad tienen los sobrinos del Pato Donald para el ejemplo pero no encontré nada :)



No hay comentarios: