01 julio, 2009

El señor del HTML 5

En CSS Squirrel, Kyle Weems explica el significado de su última viñeta. Como comentaba en un post anterior, Ian Hickson (o Hixie) es la persona que está coordinando el proyecto de redacción del nuevo estándar HTML 5. Sin embargo, Hickson trabaja para Google, lo que evidencia un posible conflicto de intereses.

Muchos de los participantes en la lista de correo acusan a Hickson de seguir una política que puede resumirse con las palabras: Negar, Retrasar, Desechar por llegar tarde.

Aunque un estándar tan importante como el que regirá el comportamiento de todos los navegadores (la cursiva es por Internet Explorer) durante los próximos años debería estar gestionado por un comité, el poder de decisión se centra en una única persona que se ha erigido como editor, y cuyas decisiones vienen muchas veces respaldadas por datos que, pese a proceder de Google, no son públicos ni se tiene la intención de que lo sean.

Estamos hablando de un esfuerzo de repercusiones casi inimaginables: miles de millones de páginas web escritas con este estándar, accesibilidad para usuarios con discapacidades, censura en países sin democracia, compartición de documentos multimedia sin la dependencia de plugins cerrados (flash)...

Estas críticas nacen de lo absurdo que resulta que una única persona sustituya a un comité de expertos, sino que además, y sin resultar una crítica a su capacidad que parece más que sobrada, el proceso -en el que muchas propuestas interesantes son rechazadas sin más o se hace mucho más caso a esos datos "secretos" que a la opinión de los expertos en cada área- dista mucho de ser transparente.

En este contexto no resulta extraño que se empiecen a alzar voces que pongan en duda la capacidad de Hickson para generar un HTML5 que cumpla con las expectativas de todos y no sólo las de Google, más cuando las críticas por el trato con los participantes (sean individuos, expertos o grupos de trabajo del W3C) no dejan de aumentar.

En esta entrevista a Hickson, sus palabras llaman poderosamente la atención:

Si respondo a comentarios sobre algo que acabo de publicar, me encuentro a veces que tengo un añadido para esa sección, por lo que si alguien sugiere un cambio total, no suele gustarme mucho esa idea. Pero si ha transcurrido un tiempo, encuentro que mi añadido ya no tiene tanta vigencia, y estoy ansioso por cambiar mi vieja estúpida idea por esa otra mejor.
Así que tengo que juzgar sobre lo que vale la pena añadir y lo que no, y eso es duro

25 junio, 2009

Programación con singletons y árbitros de eventos

He traducido el más que interesante artículo de Aaron Newton, Singletons and Event Arbiters:

¿Cuál es la mejor forma de definir código, asignarlo a una variable global y conseguir que se ejecute todo a la vez?

Usamos MooTools en el servidor en mi código ASP por medio de JScript y necesitamos objetos singleton globales como el de inicio de sesión. Puesto que no hay una forma estándar clara de hacerlo, lo he intentado y he generado algo como esto:
var LoggerObject = function() {
  // código de inicio de sesión
} 
var Logger = new LoggerObject() 
Está bien, pero parece que me estoy dejando algo. ¿Cómo lo hacen los profesionales? ¿Cuáles son las ventajas y desventajas de otros métodos?
Para los que no estén familiarizados con el concepto, un singleton (traducido literalmente sería "hijo único") es una clase diseñada para tener sólo una instancia (o un número limitado de ellas). En JavaScript, cualquier objeto puede heredar de cualquier otro objeto y no se puede prevenir que esto pase, por lo que en el sentido más estricto de la expresión, no es posible tener un singleton en JavaScript.

MooTools ofrece una función llamada "Class" que nos ayuda a gestionar la herencia entre objetos. Crear un objeto que es una instancia de una clase se consigue con el operador new:
var myWidget = new Widget()
Si lo que se desea es crear una clase e inmediatamente convertirla en una instancia, es posible hacer los siguiente:
var myInstance = new new Class({...})
El doble new invoca una función devuelta por la clase y que devuelve un objeto (una instancia de esa clase). Raramente se puede encontrar una razón para hacer esto así. De hecho, la única razón que se me ocurre es si se quiere que esa clase extienda a otra:
var myInstance = new new Class({
    Extends: SomeOtherClass,
    // nuevas propiedades aquí
});
El problema con este esquema es que no consigue mucho. JavaScript trata sobre todo con objetos, y hay mejores formas de hacer esta tarea.

Yo uso objetos (como singletons) continuamente para mi código de aplicación (obviamente casi nunca para plugins, que se supone que deben extenderse e instanciarse). Uso objetos "site" para apuntar métodos para gestionar el estado (como por ejemplo si el usuario ha iniciado sesión, su nombre de usuario, etc). Simplemento hago un objeto JavaScript básico, así:
var mySite = {
    login: function(username){ this.username = username; },
    showWelcome: function(){
        $("welcome").set("html", "Welcome " + this.username);
    }
};
No hay nada especial aquí - JavaScript funciona así. Es importante fijarse que las clases en MooTools existen para darnos funcionalidad y dejarnos derivarla a través de la herencia. Si creo una clase llamada Widget y luego quiero crear una versión llamada Widget.Ajax, puedo extender Widget y añadir sólo las partes ajax. Con los singletons el tema está en que no vas a crear más que uno.

En cualquier caso, hay algunas otras cosas que la Class de MooTools nos ofrece, como mixins (clase que ofrece cierta funcionalidad para ser heredada por una subclase, pero no está ideada para ser autónoma). Ejemplos de esto serían Event y Option que ofrecen a las instancias métodos como setOptions o addEvent. En estos casos aún puedes usar la declaración de objeto de arriba y extenderlo:
var mySite = {
    login: function(username){
        this.username = username;
        this.fireEvent("login");
    },
    showWelcome: function(){
        $("welcome").set("html", "Welcome " + this.username);
    }
};
$extend(mySite, new Events());
Aunque generalmente yo hago justo de la otra forma porque creo que es un poco más limpio (aunque se consigue el mismo efecto):
var mySite = new Events();
$extend(mySite, {
    login: function(username){
        this.username = username;
        this.fireEvent("login");
    },
    showWelcome: function(){
        $("welcome").set("html", "Welcome " + this.username);
    }
});
Con esta forma se evita la necesidad del tosco patrón new new Class y resulta más legible en mi opinión.

Árbitros de eventos

Lo que me lleva a una práctica común que suelo usar cuando construyo aplicaciones. Cuando construyo un sitio o aplicación, intento hacer las cosas lo más modulares posible. Lo hago de forma que primero mi sitio funcione sin JavaScript, y entonces empiezo a añadir los elementos de la interfície con ajax y animaciones y tablas ordenables y todo esa fiesta. También hago que el JavaScript mismo sea modular. Todo lo que pueda ser una clase, hago que sea una clase. Lo que queda es el código que instancia esas clases.

Pero ¿qué pasa si el código que instancia un widget necesita información de otro widget? Por ejemplo, ¿qué hacer si nuestro widget welcome (bienvenida) necesita saber el estado del widget de inicio de sesión? Bueno, si nuestro widget de inicio de sesión almacena su estado en el objeto mySite, entonces el widget de welcome puede inspeccionarlo, ¿no? Eso está bien; si la página carga y mySite.username no está inicializado (undefined) entonces el widget de welcome sabe que el usuario no ha iniciado sesión. Todo correcto.

Pero ¿qué pasa cuando el estado cambia? ¿Qué pasa cuando el usuario inicia sesión? Ahora necesitamos un evento - onLogin o similar, pero eso significa que Welcome necesita apuntarse al widget de inicio de sesión y entonces hemos perdido algo de modularidad al introducir una dependencia. Welcome no puede funcionar sin el inicio de sesión. En este caso, no parece que se trate de una mala dependencia - después de todo, un mensaje de bienvenida sin una funcionalidad de inicio de sesión no tiene mucho sentido - pero podría haber docenas de otros widgets que necesiten hacer cosas cuando el usuario inicia sesión y podríamos queere que esos widgets funcionen incluso si el usuario no ha iniciado la sesión. Es más, podríamos querer cargar el widget de inicio de sesión bajo demanda - cuando el usuario intente iniciar sesión por ejemplo. No es posible hacer eso si todos nuestros widgets necesitan apuntar eventos al widget de inicio de sesión.

Esta es la razón por la que hago que mySite sea una instancia de Events. mySite realmente no tiene eventos nativos en sí mismo. En su lugar, otras clases le apuntan eventos y disparan eventos por ella. Así, en nuestro puzzle de arriba sobre nuestro widget de inicio de sesión, en lugar de que todos los widgets se apunten a un evento onLogin, pueden apuntarse a mySite, y entonces nuestro widget de inicio de sesión dispara ese evento pero no sobre sí mismo sino sobre mySite:
var mySite = new Events();
var Login = new Class({
    //...login logic and stuff
    loginSuccess: function(username){
        mySite.username = username;
        mySite.fireEvent("login");
    }
});
var Welcome = new Class({
    initialize: function(){
        this.showMessage();
        mySite.addEvent("login", this.showMessage.bind(this));
    },
    showMessage: function(){
        $("welcome").set("html", mySite.username ? "Welcome " + mySite.username : "Please log in");
    }
});
Ahora nuestras clases ya no dependen entre ellas. Todas dependen ahora de mySite pero eso ya pasaba antes. Ahora pueden venir y marcharse cuando quieran sin preocuparse por las demás. Este incremento de la modularidad se agradece cuando el sitio crece en complejidad. Algunas páginas podrían tener algunos widgets y algunos podrían tener otros. Puede apuntar la lógica al objeto mySite sin tener que lidiar realmente con esas dependencias.

20 junio, 2009

La desfachatez de la E azul

Está mal abandonar a los usuarios de Internet Explorer durante cinco años (Agosto 2001-Octubre 2006), lo que podríamos equiparar a que un fabricante de automóviles no diseñe un nuevo vehículo durante 30 años. Y está mal despertar del letargo sólo porque la competencia ha estado haciendo los deberes hasta el punto de conseguir arrancar un pedazo del pastel pese a la integración de Windows con el sistema operativo y su ya famoso icono azul titulado "Internet" sin más.



Las versiones 7 y 8 se han sucedido más rápidamente, pero el camino que ahora debe recorrer Microsoft para ponerse al día con los estándares es, todavía, muy largo. Con una tendencia muy marcada a inventar por su cuenta, su último navegador queda muy atrás en cuanto a velocidad o respeto a esos estándares, a los cuales, por cierto tiende a menospreciar tal y como indica claramente Ian Hickson (el responsable de la especificación HTML5) en esta entrevista:

Personalmente me gustaría que Microsoft se implicase más con HTML 5. Han enviado muy pocas respuestas a lo largo de los años, muchísimas menos que el resto de fabricantes. Incluso cuando se les ha solicitado su opinión sobre características que estan implementando, raramente recibo una respuesta. Es muy triste. Si les envío un e-mail con una pregunta sobre qué puedo hacer para ayudarles, normalmente no recibo respuesta; como mucho recibo una promesa de que me contestarán más adelante, pero eso es todo.

En este contexto en el que siguen manteniendo una más que importante cuota de mercado (recordemos que para muchos usuarios, "Internet" sigue siendo ese icono con una "E" azul) al tiempo que mantienen un desprecio evidente ante los estándares acordados por todos, resulta especialmente provocativo lo que ha sucedido esta semana.



Mentiras para vender: Firefox viejo, Safari aburrido y Chrome oxidado


Hace relativamente poco que la versión 8 de su navegador está disponible para descargar, y Microsoft no deja de ser una casa de software con un departamento tradicional de márketing. Está demostrado que la inmensa mayoría de internautas no sabe distinguir el navegador de un motor de búsqueda como Google. Sin embargo, su nueva campaña está basada en insultos a la competencia (Genbeta, Ajaxian, WebReflection) y falsedades para encubrir las enormes carencias del navegador de Microsoft.

Probablemente Microsoft se encuentra en la incómoda situación en la que no quiere invertir en un producto que no le genera beneficios y que además amenaza a Windows, pero tampoco desea que la competencia siga en esa vorágine de mejoras continuas en la que está sumida y que poco a poco están desplazando su producto gratuito. Con esos precedentes, quizá pueda entenderse la decisión de atacar a la competencia como un intento desesperado de conservar cuota de mercado con un navegador que no quiere evolucionar.

Por ello resulta enormemente ofensiva la página de promoción del nuevo Internet Explorer y la comparativa con la competencia, plagadas de incorrecciones, medias verdades y, ¿por qué no decirlo?, auténticas mentiras para intentar demostrar lo indemostrable: que IE no sólo está a la altura del resto (recordemos que llevan 5 años de retraso) sino que además es mucho más mejor que ellos. Aunque para "demostrarlo" tachen de mitos...
  • la lentitud (más que demostrada con todas las comparativas de estos últimos meses)
  • la seguridad (todos hemos visto instalarse "software" automáticamente al visitar una página con IE)
  • la menor riqueza respecto a Firefox (ya quisieran tener una décima parte de las extensiones de éste)
  • y que su navegador no respeta los estándares, ante lo que tienen la enorme desfachatez de afirmar que "Internet Explorer 8 pasa más casos de prueba del W3C que cualquier otro navegador".

Lo peor de todo es la sensación de que, conscientes de que no van a alcanzar el nivel al que ahora mismo están Chrome, Safari, Opera o Firefox, optan por la opción más fácil: invertir en desprestigiar a la competencia, aunque con ello detengan el progreso de las tecnologías web durante unos cuantos años más. Quizá legalmente toque callar, pero moralmente merecen que su flamante E azul deje para siempre de estar unida a la palabra Internet. Lástima que sólo siga siendo un deseo...

17 junio, 2009

Opera Unite, no tan buena idea

Han habido otros intentos anteriores de introducir un servidor en el mismo navegador. ¿Para qué nos puede servir eso? Pues entre otras cosas para obtener una aplicación web que se ejecute en el mismo equipo, sin conexión... pero ¡espera! ¿No era eso lo que promete Gears y buena parte del estándar HTML5? Visto desde este punto de vista, Opera no ofrece más que Chrome que ya incluye Gears.

Por otra parte, Opera lleva innovando desde hace años y muchas de las características que hoy día traen los navegadores (pestañas, página personalizada de nueva pestaña, bloqueos de popups o sesiones) fueron inventados por ellos. Incluso en la versión anterior osaron integrar un cliente de BitTorrent. En este contexto, el de compartir, el nuevo Opera Unite puede ser un invento importante: compartir fácilmente información residente en nuestro propio equipo sin tener que preocuparse de proxies y otros problemas tradicionales (algo ya resuelto por no-ip.com). ¿Pero no hay otras tecnologías más apropiadas para ello como FTP o el propio BitTorrent? En cualquier caso tampoco es una idea nueva.


Instalado la beta de Opera 10 y activado el servicio a través del registro de una cuenta, obtenemos una página de inicio con la forma unite://equipo.usuario.operaunite.com/_root/content/ que resulta accesible desde cualquier punto de Internet cambiando el protocolo de unite:// a http://. Pero para evitar tener que pelearse con configuraciones de routers y demás, la información viaja a través de los servidores de Opera, aunque se esté sirviendo desde el mismo equipo desde el que se accede, lo que reduce sensiblemente la velocidad de respuesta. Luego hay otros puntos por resolver, como la seguridad y la disponibilidad, que evidentemente depende de que el equipo esté funcionando y con Opera corriendo en todo momento.

Los servicios que ofrece son:

  • Compartición de archivos: se selecciona una carpeta local con el contenido a compartir. Permite seleccionar quién tiene acceso y cómo (público, limitado con contraseña o sólo para el equipo local)
  • Nevera: para dejar notas al propietario del servidor (no deja de ser como el e-mail)
  • Reproductor de multimedia: para acceder a la librería de música y películas remotamente con las mismas posibilidades que la compartición de archivos
  • Compartición de fotos: ídem que el anterior pero con fotos
  • Un chat de grupo con control del propietario y alojado en el propio equipo
  • Servidor web: permite indicar una carpeta que contendrá proyectos web que pueden ser publicados para wur otros usuarios los visiten. Sin embargo, hay que tener en cuenta que los servicios ofrecidos son muy limitados (páginas estáticas)
  • La idea es que otros servicios sean añadidos en el futuro, por parte de Opera o de terceros mediante JavaScript ejecutado en el servidor en forma de Widgets

Personalmente no me gusta la idea tal y como Opera la ofrece. Sin duda es mucho más potente y con muchas más posibilidades que la tecnología de almacenamiento local y utilización del navegador sin conexión que ofrece HTML5/Gears. Igualmente hay que tener en cuenta que se trata de una beta. Pero quizá algunas de las decisiones técnicas tomadas se alejan un poco de los estándares que han estado proponiéndose en los últimos tiempos, desde el W3 Database API hasta la idea de empaquetar en un mismo archivo todo el código, evitando absurdamente una de las mayores ventajas del desarrollo web: usar un simple editor de texto para modificar el código, guardarlo y ver los cambios en el navegador.

04 junio, 2009

El proyecto Vice-versa

He traducido la introducción del Proyecto Vice-versa presentado en este post de Andrea Giammarchi. Se trata de una librería que nace con la idea de aproximar las implementaciones de las características JavaScript disponibles desde cada navegador.

El proyecto Vice-versa


Cada navegador tiene alguna característica deseable: puede ser su rendimiento, puede ser algo que "aún no se ha estandarizado", puede ser una característica preparada para ECMAScript 5. El proyecto Vice-versa pretende traer lo bueno de cada navegador a un único archivo JavaScript ligero y que valga para todos los navegadores.

Filosofía


Array.forEach(document.all, function(node, i, all){
    // proyecto vice-versa
});
Como un Ninja JavaScript tengo que tratar diariamente con centenares de problemas diferentes a causa de "esta o aquella" implementación por parte de un navegador. La web está llena de librerías pero a menudo lo que necesitamos hacer requiere usar el navegador "tal cual" y estas librerías puede ayudar o simplemente pueden hacer más lentas las aplicaciones a causa de sobrecargas innecesarias sobre tareas simples o comunes. Estudiando el DOM, que es caótico, a menudo "viajo" entre los sitios de MDC y de MSDN para resolver un problema específico que puede causar muchos dolores de cabeza a causa de "alguna" implementación del estándar W3C que alguien ha olvidado realizar. Al mismo tiempo, teniendo en cuenta que el navegador más usado es el que tiene el motor de JavaScript más lento, siempre intento encontrar soluciones que puedan completar una tarea sin afectar al rendimiento. En la mayoría de los casos, he notado que, ya que otros navegadores son más rápidos y más potentes, tiene sentido traer funcionalidades no estándar a esos navegadores en lugar de implementar, cuando sea posible, una funcionalidad omitida en Internet Explorer. Como ejemplo básico, para convertir un documento XML en una cadena, nos gustaría usar una instancia de XMLSerializer, pero ¿qué problema hay con el atajo del atributo xml de Microsoft?

// al estilo de otros navegadores (estándar)
var xmls = new XMLSerializer;
var string = xmls.serializeToString(myXMLDocument);

// al estilo de Internet Explorer (no estándar)
var string = myXMLDocument.xml;
Es más, document.evaluate es una característica muy buena, pero la llamada más común a esta función es la siguiente:

var xpresult = document.evaluate(XPathSelector, myXMLDocument, null, 0 /* as ANY_TYPE */, null);

// En internet explorer hay una función ligeramente diferente: selectNodes
var xpresult = myXMLDocument.selectNodes(XPathSelector);
La diferencia principal es el número de caracteres necesarios para llamar al método estándar en lugar de el de IE, aunque el resultado es exactamente el mismo durante las iteraciones (donde IE usa nextNode, el resto usa iterateNext). De nuevo, si estamos en Internet Explorer podemos confiar en la propiedad más fea que hayamos visto nunca desde la primera implementación de JScript sonbre el DOM: document.all. Ahora, ¿por qué demonios nos encanta la "miniaturización" de YUICompressor más gzip pero preferimos una sintaxis sin sentido como document.getElementsByTagName("*") en lugar de document.all ?

La lista podría seguir con execScript para evaluar código en el ámbito global (relacionado con tiempo de ejecución y con Ajax) y con Object.defineProperty, pero al mismo tiempo tenemos carencias con Array.prototype desde hace una eternidad, algo relativamente aburrido ya que el prototipo del constructor global debe permanecer "intocable" por razones de compatibilidad ... Ya es hora de comprender que nadie hace un bucle for in sobre listas o colecciones, así que Array.prototype ha de ser estandarizado, no hay "puede ser" o "quizá", especialmente ahora que se ha implementado como estándar en ECMAScript 5. Como resumen, estos son conceptos dentro del proyecto vice-versa: especialmente si estos comportamientos pudieran enlentecer el rendimiento general de las aplicaciones, o si estos comportamientos no son estándares.

Características vice-versa provenientes de Internet Explorer


  • Object.defineProperty (IE 8 o superior)
  • document.all, como atajo a document.getElementsByTagName("*")
  • document.createElement con soporte html para cada navegador (a veces simplifica las cosas p.e. document.createElement('<em class="test"></em>'))
  • HTMLElement.attachEvent y detachEvent
  • objeto global window.event poblado via attachEvent con las propiedades cancelBubble y returnValue
  • HTMLElement.outerHTML con set y get (la forma más simple de leer un elemento del DOM como una cadena)
  • HTMLElement.innerText con set y get (la forma más rápida en IE de insertar un nodo de texto)
  • HTMLElement.outerText con set y get (la forma más rápida en IE de reemplazar un nodo genérico por uno de texto)
  • execScript para evaluar en un ámbito global cualquier tipo de cadena (sin soporte a un segundo argumento)
  • XPath selectNodes y nextNode via documentos XML
  • propiedad xml, sólo get, para recuperar una cadena de un XMLDocument/XMLNode
  • insertAdjacentElement, insertAdjacentHTML, y insertAdjacentText, la forma más rápida de poner algo antes de un nodo, como firstChild, como lastOne, o tras el mismo nodo

Características vice-versa principalmente para Internet Explorer


  • Array.slice, para trocear todo sobre una colección genérica (compatible con DOM)
  • Array.forEach, para recorrer una colección genérica via callback (compatible con DOM)
  • Array.prototype, actualizado a la última revisión de Mozilla/FireFox (sin la especificación completa, versión enfocada en rendimiento)
  • XMLHttpRequest, para el antiguo IE6 (desfasado, será eliminado tan pronto como se pueda, pero debemos esperar primero hasta el final de este navegador)
  • setInterval y setTimeout con argumentos extra como los demás

Características vice-versa de ECMAScript 5


  • Object.getPrototypeOf, para recuperar el prototipo heredado por un objeto genérico
  • Object.keys, compatible (incluyendo toString), para recuperar un Array de claves a partir de un objeto
  • Object.create, compatible (incluyendo toString), para crear una nueva instancia a partir de otro objeto
  • Array.isArray, para conocer si un objeto genérico es un Array
  • String.prototype.trim, una implementación rápida de recorte para cada cadena
  • Function.prototype.bind, un método para asegurar el ámbito this en una llamada a función

Características extra en vice-versa


  • document.query, librería esencial de selectores para obtenere resultados genéricos mediante consultas comunes de tipo dolar (función $)
  • Object.forIn, para recorrer una instancia genérica sin considerar al prototipo heredado y sin olvidar las propiedades predefinidas (toString, valueOf, etc)

Metas de vice-versa


Pronto llegarán más funcionalidades, esperando mantener por debajo de 10KB de tamaño total minificado y comprimido (actualmente está en 4KB). Esta es sólo una implementación parcial de mi idea original. Las librerías de terceras partes serán bienvenidas, y los colaboradores de otros equipos de librerías serán más que bienvenidos. La idea principal es crear un híbrido completo del escenario JavaScript haciendo que los rendimientos sean similares en cada navegador y ofreciendo una estructura de bajo nivel para construir librerías complejas, aplicaciones, ... respetando donde sea posible a Internet Explorer, y trayendo sus características dentro de otros navegadores rápidos y potentes. ¿Estoy loco?

31 mayo, 2009

Wave reinventa la comunicación en la red

Hace unos meses me gustaba seguir a Jon Udell al respecto de posibles herramientas que permitiesen evolucionar el omnipresente correo electrónico martirizado por el spam. Proyectos como Zoë, Yammer o Groove han intentado ya mejorar ese gigante.

Google Wave es una nueva idea experimental y abierta que pretende rediseñar el futuro de la comunicación en tiempo real sobre la web. "Es lo que parecería el correo electrónico si se inventase hoy".

Wave es a partes iguales conversación y documento. Es posible comunicarse y trabajar juntos sobre texto con formato, fotos, vídeos, mapas y, prácticamente, cualquier cosa que pueda representarse sobre un navegador. Un documento wave puede compartirse. Puede responderse desde cualquier lugar en el que el mensaje esté publicado (por ejemplo un artículo en blogger, en el que los comentarios se introducen automáticamente en la conversación), editar su contenido o añadir nuevos participantes en cualquier momento. Y puede reproducirse su evolución para ver cómo ha ido evolucionando el documento a lo largo del tiempo, viendo quién ha dicho qué y cuando. Además permite la transmisión de información en tiempo real, de forma que puede verse cómo los demás teclean, agilizando las conversaciones y facilitando enormemente el trabajo sincronizado.

Wave ha sido diseñado como un estándar similar al e-mail, es abierto, y puede ser usado en cualquier servidor. Y puesto que el estándar define un protocolo, distintos servidores pueden entenderse entre ellos aunque no pertenezcan a Google.

Aunque aún no está disponible, en la presentación se muestra cómo Wave consigue mejorar enormemente el correo electrónico y la mensajería instantánea, los dos pilares básicos sobre los que se asienta. Y construye sobre ellos una plataforma de innovación totalmente abierta en la decenas de detalles e ideas han sido implementados para mejorar la experiencia de los usuarios cuando quieren comunicarse. Por ejemplo, contestar un mensaje es tan simple como escribirlo en el área prevista para ello (no hay que pulsar un botón y esperar que la aplicación cargue un formulario... simplemente "ya está ahí"); o la implementación de múltiples atajos basados en "arrastrar y soltar" convierten la experiencia en mucho más intuitiva. Aunque no deja de recordar a un cliente de correo electrónico tradicional pero optimizado, puede convertirse en un chat de forma transparente si los usuarios están conectados (de hecho los mensajes los ve el otro lado conforme se teclean, aunque esta opción puede desactivarse).

Cosas que pueden hacerse con wave:

  • reproducir la conversación paso a paso para que alguien incorporado pueda entender completamente el tema
  • mensajes privados dentro de la conversación que sólo ven los interesados
  • compartición rápida de archivos/fotos
  • aprovecha Gears si está instalado para permitir algo tan intuitivo como arrastrar y soltar archivos desde el escritorio
  • funcionalidades basadas en contactos/robot como "bloggy" para publicar la conversación, de forma que el mismo artículo del blog se convierte en parte de la conversación: cualquier comentario allí se incorpora automáticamente, convirtiendo la conversación en pública. O un traductor simultáneo.
  • edición y control de cambios al estilo wiki (resaltados como novedad)
  • posibilidad de ver la conversación como un documento, de forma que se pueda aprovechar para la creación de documentos en equipo
  • posibilidad de trabajar con equipos sobre subdocumentos del documento original, de forma que se trate de una parte del documento principal hasta su versión definitiva, que vuelve al original y así puede ser usado o modificado por otros equipos o subdocumentos
  • posibilidad de enlazar desde la actual conversaciones simplemente arrastrando y soltando
  • todo sincronizado en tiempo real, una nueva conversación puede aparecer en los resultados de una búsqueda después de haberla lanzado
  • ortografía basada en diccionarios pero también en contexto. Especialmente espectacular: "icland is an icland" se convierte en "Iceland is an island".
  • sistema de extensiones similar al del propio navegador (instalables)
  • extensiones como realizar votaciones o juegos de red de competición (ajedrez) o colaborativos (sudoku) donde los eventos son reconocidos por wave y por tanto se puede reproducir una partida como si fuese una conversación
  • conversaciones que muestran formularios de configuración con opciones que pueden rellenar más de un usuario a la vez. Es la respuesta a la pregunta de siempre: ¿Cómo creo una encuesta rápidamente?
  • integración de otros sistemas de comunicación (email, twitter) o con sistemas de depuración de errores (bugtrack) para equipos
  • posibilidad de traducción simultánea (al vuelo) de una conversación para usuarios que hablen en distintos idiomas

Otro "detalle" es que se exige un navegador moderno con soporte de HTML5. La consecuencia inmediata es que, aunque aún no está disponible, si los internautas en masa quieren disponer de esta herramienta, deberán decir adiós a Internet Explorer a menos que Microsoft, por fin, se tome en serio su trabajo.

28 mayo, 2009

A partir de JS PlaceMaker que es un script que permite identificar lugares a partir de un texto y que devuelve objetos con su ubicación, he creado el siguiente ejemplo:

http://jsbin.com/abumi/


El código es sencillo:

function callback(e) {  
  if (e.match.length) e= e.match; else e=[e.match]; 
  var gmapsApi= "ABQIAAAAGoC4gjFriF0K4FcpSOhWDhTWx8njL9qYLnq46LoOVGM6mIGTuxR2yPgKzvzjHKLSfLHgjUC0vi45-Q"; 
  var imgs=''; 
  console.dir(e); 
  for (loc in e) { 
    center=e[loc].place.centroid.latitude+','+e[loc].place.centroid.longitude; 
    imgs+= '<img src="http://maps.google.com/staticmap?center='+center+'&markers='+center+'&zoom=13&size=350x250&key='+gmapsApi+'" style="float:left;"/>'; 
  } 
  document.getElementById('map').innerHTML=imgs; 
  window.called=false; 
} 
 
window.called=false; 
function changed() { 
  if (window.called) return; 
  setTimeout(function(){ 
    Placemaker.config.appID = "jPkyQyTV34Fi4qlev38PC_eXLDQYqVPkQ5N1aZDxVUSpw3MKOUWD9AHzcVkLFkM-"; 
    Placemaker.getPlaces(document.getElementById('text').value,callback,document.getElementById('locale').value); 
    window.called=true; 
    }, 
  1000); 
}

Puedes probar con el texto: "I'm Jack London and I live in Barcelona but I work at Amsterdam and my parents live in Oslo", (nótese que detecta que la palabra London no se está usando como ubicación) aunque hay un selector para indicar el lenguaje del texto introducido. La detección se produce al salir del campo de texto, aunque también se puede forzar con el botón. Las claves son para el dominio del contenedor de código jsbin.com

26 mayo, 2009

Compresión JSON con JSON.hpack

JSON.hpack es un compresor de conjuntos de datos JSON. Es capaz de reducir hasta un 70% el número de bytes necesarios para representar una colección genérica y homogénea (entendiendo por homogénea la que suele ser producida por una consulta a una base de datos relacional en la que se repite una serie de filas con el mismo número de campos).

Así como un resultado con XML como el siguiente...

  <result>
    <item>
      <name>Andrea</name>
      <age>31</age>
      <gender>Male</gender>
      <skilled>true</skilled>
    </item>
    <item>
      <name>Eva</name>
      <age>27</age>
      <gender>Female</gender>
      <skilled>true</skilled>
    </item>
    <item>
      <name>Daniele</name>
      <age>26</age>
      <gender>Male</gender>
      <skilled>false</skilled>
    </item>
  </result>
  <!-- 286 caracteres (sin contar espacios) -->

... puede ser compactado con JSON a la cadena siguiente...

[
  {
    "name":"Andrea",
    "age":31,
    "gender":"Male",
    "skilled":true
  },
  {
    "name":"Eva",
    "age":27,
    "gender":"Female",
    "skilled":true
  },
  {
    "name":"Daniele",
    "age":26,
    "gender":"Male",
    "skilled":false
  }
]
// 177 caracteres (sin contar espacios)

... podemos ver fácilmente que JSON admite una compresión aún mayor, puesto que cada registro repite los nombres de los campos.

El nivel 0 de compresión con JSON.hpack propone reunir los nombres de los campos en un primer registro:

[["name","age","gender","skilled"],["Andrea",31,"Male",true],["Eva",27,"Female",true],["Daniele",26,"Male",false]]
// 115 caracteres

Aún así, se repiten valores (como true, false, male y female) que podrían ser sustituidos por índices para ahorrar aún más espacio (aunque en este ejemplo con 3 registros el resultado no es mejor que el anterior):

[["name",["Andrea","Eva","Daniele"],"age","gender",["Male","Female"],"skilled",[true,false]],[0,31,0,0],[1,27,1,0],[2,26,0,1]]
// 167 caracteres

Como se puede ver en este nivel 1 de compresión, en el primer registro se indican los nombres de los campos y los posibles valores que pueden adoptar siempre que no sean numéricos (el autor cree que no vale la pena realizar la conversión de unos números por otros (índices)). Por ejemplo, Male o True se sustituyen por 0 y Female o False por 1.

Esto aún puede optimizarse más con el nivel 2 de compresión, comparando si vale la pena enumerar los valores en la cabecera y sustituirlos en los datos por índices o si es mejor mantener los datos (en el siguiente caso, los nombres no se enumeran):

[["name","age","gender",["Male","Female"],"skilled",[true,false]],["Andrea",31,0,0],["Eva",27,1,0],["Daniele",26,0,1]]
// 119 caracteres

Esta comprobación se realiza a nivel de la colección entera. Es posible realizarla a nivel de cada campo, comparando la longitud de la enumeración más los índices que serán necesarios con los de indicar directamente los valores:

// Comparando "gender":
  ["Male","Female",0,1,0] < ["Male","Female","Male"] (23 caracteres es menos que 24)
// comparando "skilled" :
  [true,false,0,0,1]      > [true,true,false]        (18 caracteres es menos que 17)

Según esto, el resultado de aplicar el nivel 3 de compresión será:

[["name","age","gender",["Male","Female"],"skilled"],["Andrea",31,0,true],["Eva",27,1,true],["Daniele",26,0,false]]
// 116 caracteres

Y aún se puede extraer un último nivel 4 de compresión comparando todos los anteriores y seleccionando el que menos ocupe. Es posible hacer pruebas en la página de ejemplos.

Aunque parece que la compresión es importante, hay que tener en cuenta que si el contenido se está comprimiendo con gzip desde el servidor, la reducción real en información enviada será mucho menor. Para conjuntos grandes de datos, sí que puede haber una diferencia importante, aunque se pague el precio del procesamiento necesario para comprimir y descomprimir los datos.
Existen librerías para JavaScript, PHP y C#, aunque según el wiki del proyecto, es posible que se amplíe a Python, Ruby o plugin PHP mediante C.

En concreto, la versión JavaScript cuenta con los siguientes métodos:

  • hpack( Array[, Int[0-4]] ):HArray, convierte una colección homogénea a otra comprimida con hpack
  • hunpack( HArray ):Array, convierte una colección hpack a su original (no es necesario indicar qué compresión se usó)
  • hbest( Array ):Int[0-4], devuelve el nivel de compresión ideal para la colección homogénea especificada

21 mayo, 2009

Mozilla Jetpack


Pese a la dificultad de escribir extensiones para Firefox y que su uso aún requiere reiniciar el navegador, lo cierto es que el sistema de extensiones ha supuesto un antes y un después en la historia de los navegadores.

Google Chrome promete cambiar de golpe ambos problemas (la dificultad de creación y el engorro de reiniciar el navegador) mediante un sistema que se basa en las herramientas que ya están disponibles en el navegador: HTML, Javascript, CSS...

Lo cierto es que la idea era demasiado buena para que los responsables de Firefox se quedasen con los brazos cruzados, por lo Mozilla Labs acaba de publicar la primera preview de Jetpack.



Jetpack es un nuevo experimento que utiliza tecnologías abiertas para mejorar el navegador, con la meta de permitir que cualquiera que pueda crear una web pueda participar en hacer que la web sea un un mejor sitio para trabajar, comunicarse y jugar. En breve, Jetpack es una API para facilitar la creación de extensiones de Firefox usando tecnologías con las que ya estás familiarizado.

Los objetivos son los siguientes:

Basado en la Web

  • HTML, Javascript, y CSS serán las únicas herramientas necesarias
  • Tener toda la potencia de la Web abierta: capacidades AJAX y multimedia incluyendo <canvas>, <audio> y <video>
  • Ligero y accesibles mediante una URL, como el resto de la Web
  • Depuración en el navegador sin reiniciarlo y usando herramientas comunes de desarrollo Web como Firebug

Seguro

  • Permite acceso sólo a los privilegios necesarios, sin temas de seguridad presentados de forma social y no técnica
  • Código breve y sencillo de revisar que asegura que los potenciales problemas de seguridad sean superficiales y los tiempos de revisión breves

Robustez

  • APIs versionadas de forma que no sea necesario actualizar y revalidar el código con cada nueva versión de Firefox
  • Ligero pero lleno de características, permitiendo que aplicaciones complejas y simples se sumen a la experiencia Web, igual que las extensiones hoy día

Extensible

  • La arquitectura permitirá la inclusión de toolkits de terceros revisados y versionados (p.e. jQuery, Dojo, etc.) y de librerías API (p.e. Twitter, Delicious, Google Maps, etc.)

Ejemplo de extensión (un notificador de Gmail):
// Inicia el componente de barra de estado

jetpack.statusBar.append({
  html: '<img src="http://mail.google.com/mail/images/favicon.ico"><span id="count"></span>',

  onReady: function(doc) {
    var gmail = new GmailNotifier(doc);
  },

  width: 20
});

// Notificador de Gmail

function GmailNotifier(doc){
  $(doc).click(this.goToInbox);
  this.update(doc);
  setInterval(function() {
    this.update(doc);
  }, 60 * 1000);
}

GmailNotifier.prototype = {

  goToInbox: function() {
    jetpack.tabs.open("http://mail.google.com");
    jetpack.tabs[ jetpack.tabs.length-1 ].focus();
  },

  update: function(doc) {
    var url = "http://mail.google.com/mail/feed/atom";
    doc = $(doc);
    $.get( url, function(xml){
      var el = $(xml).find("fullcount"); // Cuenta de mensajes por leer
      if (el) {
        var count = el.get(0).textContent;
        doc.find("#count").text( count );
      } else {
        doc.find("#count").text( "Login" );
      }
    });
  }
}
Via Ajaxian.

Addendum: Elijah Gray ha recopilado la información sobre la API que no está completa en la página oficial y la ha publicado en su blog.

13 mayo, 2009

BrowserCouch

En Day Dreaming about Web Storage, Mark Finkle trata la naturaleza de la futura API de almacenamiento de datos en el navegador. Sea finalmente SQL, como abogan algunos, o un lenguaje más coherente con la idiosincrasia de Javascript y JSON, como abogan otros, la esperada API deberá ser más bien simple para que permita un ecosistema de librerías que, al igual que los actuales frameworks como MooTools o jQuery, ofrezca diversas opciones que se adapten a todas las necesidades y gustos.

Aunque SQL está mucho más extendido, proyectos como TrimQuery demuestran que no es imprescindible que el navegador incorpore un parseador para un lenguaje con tantas versiones incompatibles entre sí como fabricantes existen.

BrowserCouch es un intento de implementar la tecnología MapReduce en el navegador. Está escrito enteramente con JavaScript con la intención de que funcione con todos los navegadores, aprovechando de forma transparente otras capacidades disponibles.

No es casualidad que esta librería trate de imitar la funcionalidad de CouchDB en el cliente, e incluso podría soportar su integración en el futuro.

Esta librería es una respuesta al artículo de Vladimir Vukicevic HTML5 Web Storage and SQL. Una API al estilo de CouchDB parece una buena solución para el almacenamiento persistente en la Web, ya que mucha de su semántica es delegada al lenguaje JavaScript, lo que lo convierte en potencialmente sencillo de estandarizar. Además, el paradigma MapReduce también saca ventaja natural de los múltiples núcleos de los microprocesadores- algo cada vez más frecuente hoy día.

Tutorial


Ésta es una breve introducción al uso de la API de BrowserCouch y del mecanismo MapReduce. Hay que remarcar que no se trata de software "maduro" ya que aún faltan muchas características de CouchDB por portar y la API no es estable.

Respecto a los ejemplos de código de este tutorial: se ejecutan en el mismo navegador y los resultados se muestran en algunos casos en esta misma página. Esto permite asegurar que el software funciona como se pretende y permite un aprendizaje interactivamente. También conviene avisar que existe la posibilidad de que alguna de los ejemplos falle.

Primeros pasos


Supongamos que queremos añadir soporte sin conexión a un blog. Para obtener una base de datos llamada blog-posts en BrowserCouch se puede usar la siguiente función:

BrowserCouch.get('blog-posts', 
  function onRetrieveCb(db) { 
    blogDb = db; /* Guarda la base de datos para luego. */ 
  }, 
  new FakeStorage()
);
Está claro que el primer parámetro es el nombre de la base de datos; el segundo es la función callback que recibirá la base de datos tras obtenerse.

El tercer parámetro especifica el motor que será usado para almacenar la base de datos entre sesiones de navegación. En este caso se usa FakeStorage, el cual almacena los datos en memoria de forma no persistente, lo que vale como ejemplo. Podríamos igualmente no especificar el tercer parámetro para que BrowserCouch averigüe el mejor motor según las capacidades disponibles.

Si la base de datos no existe, se creará una nueva. Poner nuevos artículos en la base de datos se consigue con el método put():

blogDb.put( [
  {id: 0, author: 'Myk', title: 'Burritos', content: 'Burritos are yum.'}, 
  {id: 1, author: 'Thunder', title: 'Bacon', content: 'I like bacon.'}, 
  {id: 2, author: 'Thunder', title: 'Beer', content: 'Beer is good too.'}], 
  function onDone() { 
    /* Código función... */ 
  }
);
Cada elemento que guardemos en la base de datos necesita un atributo identificador, pero a parte de eso, el elemento puede contener cualquier dato codificable como JSON.

Vistas

Ahora que tenemos datos, podemos jugar con la generación de vistas sobre los datos usando el mecanismo MapReduce. Por ejemplo, a continuación se define una vista definiendo sólo la fase de mapeo que organiza todos los títulos de artículos por autor:

blogDb.view({ 
  map: function(doc, emit) { 
    emit(doc.author, doc.title); 
  }, 
  finished: function(result) { 
    displayInElement(result, 'author-keyed-view'); 
  } 
});
El método view() tiene muchos argumentos opcionales, y esa es la razón por la que estamos pasando un único objeto con las claves que se corresponden con los nombres de los argumentos. El argumento map es la función usada en la fase de mapeo, y el argumento finished es la función callback que recibirá los resultados cuando termine el proceso.

El resultado obtenido en el elemento author-keyed-view será:

{"rows":[
  {"id":0,"key":"Myk","value":"Burritos"},
  {"id":1,"key":"Thunder","value":"Bacon"},
  {"id":2,"key":"Thunder","value":"Beer"}
]}

Como se puede ver, BrowserCouch esencialmente itera sobre todos los objetos de artículo, pasándole cada uno a la función map() junto con una función arbitraria llamada emit(). La función map() entonces decide si el par clave-valor debe aceptarse pasándolo a la función emit(). map() puede hacer tantas llamadas a emit() como desee: cada llamada generará una nueva fila en la vista.

En este punto quizás resulte interesante saltar a la sección de pruebas para jugar definiendo una función map() personalizada. Edita el código y al salir del campo de texto se actualizará el resultado.

La fase de reducción de una vista es totalmente opcional y un poco confusa. Intentemos añadir una función reduce() a nuestra vista para agrupar juntos los títulos de artículos con los autores:

blogDb.view({ 
  map: function(doc, emit) { 
    emit(doc.author, doc.title); 
  }, 
  reduce: function(keys, values) { 
    return values;
  }, 
  finished: function(result) { 
    authors = result; /* Guarda los resultados para más tarde. */
    displayInElement(authors, 'author-titles-view');
  } 
});
lo que generará el siguiente resultado:

{"rows":[
  {"key":"Myk","value":["Burritos"]},
  {"key":"Thunder","value":["Bacon","Beer"]}
]}
BrowserCouch tomará todas las filas generadas por map() y genera una nueva lista de filas clave-valor, donde el valor de cada fila es la lista de todos los valores que coinciden con la clave de la fila. Esto explica el significado del argumento values pasado a reduce().

El argumento keys es una lista de tuplas de dos elementos, el primera de los cuales es la clave y el segundo es el identificador del documento que emitió la clave durante la fase de mapeo.

La función reduce() es invocada por cada clave única, y su valor de retoro es el valor de su clave en la vista final.

Una vez que se tiene la vista, se puede usar el método findRow() de la vista para encontrar la primera fila cuya clave coincida con (o sea la más parecida a) la provista. Por ejemplo:

var rowIndex = authors.findRow('Thunder'); displayInElement(authors.rows[rowIndex], 'author-find-row-view');
Y el resultado es:
{"key":"Thunder","value":["Bacon","Beer"]}

Prueba tú

Si tienes los ojos cruzados, no te preocupes - a mucha gente le lleva bastante tiempo comprender cómo funciona MapReduce. Dicho eso, la forma más rápida de comprender su funcionamiento es jugar creando tu propia vista.

En la página del proyecto tienes un campo de texto para hacerlo. Pulsa la tecla tabuladora cuando hayas terminado de hacer cambios para lanzar la consulta y ver los resultados.

12 mayo, 2009

TaffyDB

Siguiendo el debate sobre el mecanismo a introducir en los navegadores para implementar bases de datos locales, describo aquí las características y el funcionamiento de TaffyDB: TaffyDB es una librería JavaScript open source que actua como una delgada capa para aplicaciones web 2.0 y AJaX. Sus características son:

  • Tamaño de 10KB
  • Sintaxis simple centrada en JavaScript
  • Rápida
  • Fácil de incluir en cualquier aplicación web
  • Compatible con las librerías más comunes: YUI, jQuery, MooTools, Dojo, Prototype, EXT, ...
  • Interfície CRUD (Crear,Leer,Actualizar,Eliminar)
  • Ordenación
  • Bucles
  • Consultas avanzadas

Introducción rápida

Creando colecciones

Para crear una colección Taffy hay que pasar un array de objetos similares como el que devuelve un servicio web JSON, por ejemplo cuatro amigos:
var amigos = new TAFFY(
[
{nombre:"Pepe",
  genero:"H",
  casado:"No",
  edad:25,
  ciudad:"MAD",
  comidas_preferidas:["pizza","tacos"]},
 {nombre:"Eva",
  genero:"M",
  casado:"No",
  edad:29,
  ciudad:"BCN",
  comidas_preferidas:["ensalada","palitos de queso"]},
 {nombre:"Paco",
  genero:"H",
  casado:"No",
  edad:29,
  ciudad:"VLC",
  comidas_preferidas:["pizza","hamburguesas","BLTs"]},
 {nombre:"Sara",
  genero:"M",
  casado:"No",
  edad:21,
  ciudad:"ZAZ",
  comidas_preferidas:["pizza","sushi"]}
  ]
)
También acepta una cadena de texto JSON por evaluar. Siempre devolverá una colección de métodos para trabajar sobre el conjunto recibido.

Encontrar

Para ver los amigos que tienen más de 22 años:
amigos.find({edad:{greaterthan:22}});
Para realizar una consulta se debe llamar al método find con un objeto como parámetro para indicar las condiciones de filtrado a aplicar. Es similar a la cláusula where de SQL. Devuelve un array de índices a los objetos que cumplan las condiciones. Para encontrar a los amigos que vivan en Valencia, Madrid o Zaragoza:
amigos.find({ciudad:["VLC","MAD","ZAZ"]});
Que sería similar al operador IN de SQL.

Actualizar

Cómo actualizar la colección para reflejar que Pepe ahora vive en Bilbao y se ha casado:
amigos.update(
 {
 ciudad:"BIO",
 casado:"Yes"
 },
 {
 nombre:"Pepe"
 }
);
El método update recibe un objeto con los datos que hay que cambiar y otro objeto (cláusula WHERE) con las condiciones de filtrado para encontrar los objetos sobre los que hay que aplicar los cambios. También es posible indicar el índice del objeto a modificar o directamente el resultado de una búsqueda (si no se indica un segundo parámetro, las modificaciones afectarán a todos los objetos de la colección):
amigos.update({ciudad:"BIO",casado:"Yes"},1);
amigos.update(
 {
 ciudad:"BIO",
 casado:"Yes"
 },
 amigos.find(
  {nombre:"Pepe"}
  )
 );

Insertar

El método insert acepta un objeto a insertar o un array de objetos:
amigos.insert(
 {nombre:"Brian",
 genero:"H",
 casado:"No",
 edad:52,
 ciudad:"IBZ",
 comidas_preferidas:["fruta","chuleta"]
 });

Eliminar

El método para eliminar registros se llama remove dado que delete es una palabra reservada. Acepta como parámetro un objeto con las condiciones de filtrado para seleccionar los objetos a borrar:
amigos.remove({nombre:"Brian"});

Ordenar

Para ordenar hay que indicar un array con los campos de ordenación, indicando el nombre del campo si es ascendente o con la expresión {"campo":"desc"} si es descendente. Por ejemplo, para ordenar por edad ascendente y por nombre alfabético pero descendente:
amigos.orderBy(["edad",{"nombre":"desc"}]);
También es posible ordenar de forma lógica indicando como parámetro "logical" o "logicaldesc" de la misma forma como se indica "desc" para los descendentes. El método orderBy también acepta un segundo parámetro opcional con una función JavaScript como la que acepta la array.sort().

ForEach

Es un método que permite aplicar una función a cada elemento de una colección:
amigos.forEach(function (f,n) {alert(f.nombre)});
El primer parámetro de la función es el objeto y el segundo el índice del objeto dentro de la colección. Opcionalmente se pueden indicar condicionantes de filtrado para reducir el conjunto de objetos sobre el que se aplica la función:
amigos.forEach(
 function (f,n) {alert(f.nombre);},
 {comidas_preferidas:{has:"pizza"}}
);
Es posible modificar un registro, devolviendo una copia del objeto recibido desde la función. Si no se devuelve nada, el registro no se modifica. Por ejemplo, para incrementar la edad de todos los amigos:
amigos.forEach(
    function (f,n) {f.edad = f.edad+1; return f;}
);

Get/First/Last

Para obtener un conjunto de objetos que cumplan una condición de filtrado, se usará get(); first() para obtener el primer objeto que cumpla la/s condición/es, y last() para obtener el último. Si no se especifica el parámetro, las mismas funciones devolverán todos los objetos, o el primero y último de la base de datos.

amigos.get({nombre:"Pepe"});
amigos.first({nombre:"Pepe"});
amigos.last({nombre:"Pepe"});

Stringify

Funciona como get() pero devuelve el resultado como una cadena de texto JSON preparada para ser usada en servicios web / AJaX.

Templates (patrones)

Se pueden usar patrones para añadir valores por defecto a una colección y minimizar el código escrito. Un patrón es un objeto que será usado como base para nuevas inserciones. A menos que el nuevo registro sobreescriba los valores indicados en el patrón, TaffyDB usara los valores del patrón para rellenar el registro.

Para preparar un patrón llamar a collection.config.set("template",{}). Para eliminarlo se llamará a collection.config.set("template",null). Un patrón se aplicará automáticamente a cada nuevo registro de la colección.

Este ejemplo añade un patrón a la colección de amigos y establece unos valores por defecto para el teléfono y el correo electrónico. Cualquier amigo que ya tuviese definidos dichos valores no se vería afectado por el patrón:

amigos.config.set("template",
 {email:"none",
 telefono:"none"}
);
También se puede usar collection.applyTemplate para aplicar un patrón a un subconjunto de los registros. El patrón se aplicará en este caso a los registros actuales, pero no a las futuras inserciones o cambios aunque cumplan la/s condición/es indicadas:

amigos.applyTemplate(
 {"conyugue":"desconocida"},
 {casado:"Yes"}
);
Nota: es posible recuperar el patrón impuesto a una colección mediante collection.config.get("template").

Eventos

Hay tres eventos que pueden usarse:
  • onInsert(nuevoObj) - se dispara cada vez que se inserta un nuevo registro.
  • onUpdate(modificadoObj,originalObj) - se dispara cada vez que un registro es actualizado.
  • onRemove(eliminadoObj) - se dispara cuando un nuevo registro es eliminado.

Ejemplo:
amigos.onRemove = function (r) {
 alert(r.nombre + " ha sido eliminado");
};
amigos.remove();

Consultas avanzadas

Ya que casi todos los métodos de TaffyDB incluyen condiciones de filtrado, conviene comprender sus posibilidades:

Usando el método de búsqueda detallado arriba, es posible pasar varios filtros para reducir los resultados:

amigos.find({ciudad:["VLC","BCN","MAD"],
  edad:{greaterthan:22}}); //mayorque
Aquí se recibirán los amigos de las tres ciudades que tengan más de 22 años. Agregar filtros a una consulta es equivalente a usar un AND en un WHERE de SQL.

Nota: es posible invertir la lógica de filtrado añadiendo el signo ! antes del nombre, que sería similar al operador != en el SQL estándar. Esto se aplica a todos los tipos de filtrado.
Ejemplo (encontrar amigos que no sean de Madrid):
amigos.find({ciudad:{"!is":"MAD"});
Hay 17 formas de filtrar una colección:
  • equal (default) (equivalente: is) // Colección que coincida con un texto o número indicado.
  • startswith (equivalente: starts) // Colección que empieza por el texto indicado.
  • endswith (equivalente: ends) // Colección que termina con el texto indicado.
  • greaterthan (equivalente: gt) // Colección mayor que el número indicado.
  • lessthan (equivalente: lt) // Colección menor que el número indicado.
  • has // Colección que contiene el objeto, texto, o clave indicado.
  • hasAll // Colección que contiene todos los objetos, textos o claves definidas en el array indicado.
  • regexppass (equivalente: regex) // Colección que cumple una expresión regular.
  • like // Colección que incluye el texto indicado.
  • notlike // Colección que no incluye el texto indicado.
  • isSameObject // El valor del objeto colección coincide con el objeto indicado.
  • isSameArray // El valor de array de la colección coincide con el array indicado.
  • length // La longitud del valor de la colección coincide con el número indicado (leer a continuación).

Usando los filtros de longitud

Usando el filtro de longitud es posible encontrar cadenas y arrays basándose en su longitud. Las forma simple es con una coincidencia de uno a uno:

friends.find({state:{length:15});

La otra forma es utilizar uno de los métodos de arriba. Es una buena forma de encontrar longitudes por encima o por debajo de un valor:

friends.find({state:{length:{gt:10}});
Consultas basadas en tipos

La version 1.4 de TaffyDB introdujo un número de métodos para filtrar que se basan en el tipo. Ya que TaffyDB permite almacenar cualquier tipo de datos en cualquier columna, los métodos pueden usarse para filtrar por una columna multi-tipo. Las opciones para filtrar por tipo son:

  • isString // La colección es una cadena.
  • isNumber // La colección es un número.
  • isArray // La colección es un array.
  • isObject // La colección es un objeto.
  • isBoolean // La colección es a boolean (true/false).
  • isFunction // La colección es function.
  • isNull // La colección es null.
  • isUndefined // La colección es "undefined".
  • isNumeric // La colección contiene sólo números.
  • isTAFFY // La columna de colección es una colección de TaffyDB.

Para usar estos filtros se necesitará pasar un valor "true" o "false" contra el que comparar:

amigos.find({ciudad:{isArray:true}});
Construyendo consultas eficientes

Además de poder filtrar de distintas formas y combinar los filtros para llevar a cabo consultas más complejas, también se pueden optimizar las consultas pasando al método de consulta una lista con los índices contra los que ejecutar la consulta en lugar de tener que hacerlo sobre la colección entera:

amigos.find({ciudad:["MAD","BCN","VLC"],
  edad:{lt:50}},
  [0,2]);
Cada llamada al método de consulta requerirá que cada registro de la colección sea comprobado al menos una vez. Para consultas complejas y colecciones grandes, esto puede requerir mucho trabajo con bucles. TaffyDB reduce automáticamente los resultados conforme va filtrando (con el segundo filtro siendo aplicado sólo a aquellos registros que han pasado el primero, y así sucesivamente). Pero a veces eso no es suficiente. En el ejemplo anterior ya limitamos los resultados indicando un array de índices. Esta es una forma de evitar que TaffyDB recorra la colección completa si ya se conocen los registros sobre los que se va a actuar.

07 mayo, 2009

jLinq, el LINQ de Javascript

jLinq es una versión Javascript del popular lenguaje LINQ (Language Integrated Query) de Microsoft que permite consultas nativas semejantes a las de SQL en los lenguajes .NET.

En este caso, jLinq ataca a arrays de objetos Javascript en memoria y permite realizar consultas sobre ellos con una sintaxis basada en funciones encadenadas (como jQuery), pero que contiene todos los componentes para seleccionar y filtrar que ofrece SQL. Por ejemplo:

var results = jLinq.from(data.users)
    .startsWith("first", "a")
    .or("j").or("m")
    .orEndsWith("y")
    .orderBy("admin","age")
    .select();
Es fácil identificar la función from() (indica el array de objetos sobre el que se realizará la consulta), la función orderBy() (ordena los resultados a partir de los campos indicados) y la función select() (devuelve los datos filtrados hasta el momento). El resto de funciones emulan las posibilidades que SQL ofrece en la cláusula "where".

En concreto, el ejemplo anterior obtiene los usuarios cuyo nombre(first) empiece por "a", "j" o "m" o que termine por "y", y los ordena por los campos administrador(admin) y edad(age).

La demo de jLinq incluye un completo tutorial para aprender sus posibilidades paso a paso, permitiendo probar online los resultados de las consultas de ejemplo. Algunos apuntes:

  • Por defecto se entiende que las funciones de filtrado se unen por ANDs (y lógico) a menos que se especifique expresamente la función or().
  • Si queremos obtener una lista de los usuarios que son administradores junto con los que no lo son pero tienen permiso de borrado, NO podemos hacer lo siguiente ya que todo se evalua a la vez y la consulta no devolvería nada:
var results = jLinq.from(data.users) 
.is("admin") 
.isNot("admin") 
.contains("permissions", "delete") 
.select(); 
La forma correcta es:
var results = jLinq.from(data.users)  
.is("admin") 
.orCombine(function(q) { 
q.isNot("admin") 
.contains("permissions", "delete"); 
}) 
.orderBy("admin") 
.select();
  • Una forma de obtener los usuarios que empiecen por a,b,c,d,e y ordenarlos inversamente:
jLinq.from(data.users).startsWith('first', ['a','b','c','d','e']).orderBy("-first").select();
  • Para el típico join de dos tablas (aquí data.users y data.locations):
var results = jLinq.from(data.users) 
.join(data.locations, //el array fuente 
"location", //el alias a usar (cuando se haga el join) 
"locationId", // el id de localización del user 
"id" // el id de localización 
) 
.select(function(r) { 
return { 
fullname:r.first + " " + r.last, 
city:r.location.city, 
state:r.location.state 
}; 
}); 
Básicamente realiza una consulta en la tabla data.locations uniendo el campo locationId de data.users con el campo id de data.locations. También se puede observar como seleccionar los campos a visualizar dentro de la función que se pasa como parámetro al select).
  • Los comandos disponibles son:
    • especificación de origen: from
    • acciones: and, andNot, combine, debug, ignoreCase, not, or, orCombine, orNot, useCase
    • consultas: con 2 parámetros between, betweenEquals, con 1 parámetro contains, empty, endsWith, equals, greater, greaterEquals, is, isNot, less, lessEquals, match, startsWith, y where.
    • selección: all, any, at, count, distinct, each, first, groupBy, join, last, none, orderBy, select, skipTake, take, toTable
Sin duda, supone una forma programática de conseguir lo mismo que con SQL y evitar el proceso de parseado. Pero el afán por seguir el estándar SQL, aunque hay que decir que está muy extendido, impide ver las posibilidades que el propio Javascript puede ofrecer para este tipo de trabajo. Sigue resultando más claro leer una simple consulta SQL. El objetivo sería conseguir la claridad de SQL aprovechando la flexibilidad que Javascript ofrece. Ahí creo que CouchDB va mejor enfocado, aunque la claridad deja que desear.

Últimos links en indiza.com