Categories
español programacion web development

Crear una aplicación sencilla con Backbone.js

Este artículo es la versión en post del taller que dí el 6 de septiembre de 2012 en la primera edición de betabeers en Málaga. Para los que no lo conozcáis, betabeers es un evento que se realiza en varias ciudades españolas y del mundo, donde se reúnen desarrolladores para hablar de sus cosas. En esas reuniones se realizan charlas técnicas en los que se explica alguna tecnología, se presentan startups y luego hay una sesión de networking con cervezas.

En el taller que impartí introduje Backbone.js, un framework javascript para el desarrollo de single page apps. Este tipo de aplicaciones está en alza pues permite ofrecer al usuario una experiencia similar a la que ofrecen las aplicaciones de escritorio, y es de sobra conocido por todos pues, quien más y quien menos, seguramente haya usado alguna de las aplicaciones web más famosas alguna vez: GMail, Twitter, GitHub, etc…

El objetivo era dar un vistazo sobre qué es Backbone.js, explicar los modelos, colecciones y vistas y hacer una aplicación de ejemplo usando la API de YouTube. Esta aplicación carga unos vídeos del feed de YouTube y los muestra en la pantalla, todo ello controlado mediante javascript y usando el framework de Backbone.js.

Se puede encontrar una demo de la aplicación en esta dirección, y el repositorio del código se encuentra en esta otra dirección.

Aquí os dejo la presentación que usé en el taller, y a continuación paso a explicarla.

¿Pero qué es Backbone.js?

Backbone.js es un framework javascript pseudo-MVC creado por DocumentCloud. Entre sus principales características está la facilidad que ofrecen para acabar con el spaghetti-code en javascript. ¿Quién no ha tenido complicaciones con los handlers de los eventos usando jQuery? Que si en una llamada a $.ajax pongo un callback para success y otro para error, y luego cuando entre en success tengo que actualizar los elementos de la página para mostrar los datos recibidos, y todo ello con funciones inline o sueltas en los archivos de código.

No es la mejor situación para tener una buena estructura de código que ayude a que este sea más fácil de mantener.

Backbone.js ofrece una serie de objetos para poder organizar mejor el código de la aplicación:

  • Model, que contiene la información de un objeto de datos (por ejemplo un registro de la base de datos)
  • Collection, que contiene un conjunto de modelos
  • View, que se encarga de gestionar los objetos del DOM y son la parte visible de la aplicación
  • Router, que se encarga de las transiciones entre las vistas (lo que venían a ser los “cambios de página” de antaño)
  • History, que guarda el historial de navegación del usuario dentro de la single page app.

Cada uno de esos objetos ofrece además un conjunto de funciones auxiliares y gestores de eventos, de forma que se pueda gestionar la creación y el guardado de los datos (modelos), los eventos entre los propios objetos y demás.

Requisitos

Backbone.js tiene una serie de requisitos para su funcionamiento:

  • Para la gestión de las vistas necesita una librería para la manipulación del DOM como jQuery o Zepto
  • Para el procesamiento de JSON necesita la librería JSON2
  • Y como funciones auxiliares, necesita la librería Underscore, también desarrollada por DocumentCloud

La estructura de datos de la aplicación

La aplicación, como ya he dicho antes, va a cargar el feed de vídeos de YouTube y va a mostrarlos en la página. Por lo tanto, lo primero que hay que hacer es ver en qué consiste el modelo de datos de la aplicación.

La API de YouTube que voy a usar se encuentra en la siguiente URL:

http://gdata.youtube.com/feeds/api/videos?v=2&alt=jsonc

El parámetro más importante es el que indica el formato de salida que queremos para los datos, pues nuestra aplicación va a trabajar con JSON, que es el formato nativo para intercambiar datos en javascript. Si cargamos la dirección podremos ver la estructura de datos que devuelve:

 
{
  apiVersion: "2.1",
  data: {
    updated: "2012-09-05T18:07:05.928Z",
    totalItems: 1000000,
    startIndex: 1,
    itemsPerPage: 10,
    items: [
      {
        id: "trenrwaJNwM",
        uploaded: "2012-08-20T07:43:32.000Z",
        updated: "2012-08-27T17:45:45.000Z",
        uploader: "theusbrhighlights",
        category: "Entertainment",
        title: "SummerSlam 2012 - Brock Lesnar vs Triple H Highlights",
        description: "Song: Skillet - Awake and Alive All rights Reserved by WWE No copyright...",
        thumbnail: {
          sqDefault: "http://i.ytimg.com/vi/trenrwaJNwM/default.jpg",
          hqDefault: "http://i.ytimg.com/vi/trenrwaJNwM/hqdefault.jpg"
        },
        player: {
          default: "http://www.youtube.com/watch?v=trenrwaJNwM&feature=youtube_gdata_player"
        },
        content: {
          5: "http://www.youtube.com/v/trenrwaJNwM?version=3&f=videos&app=youtube_gdata"
        },
        duration: 199,
        aspectRatio: "widescreen",

Podemos ver que la consulta nos devuelve un objeto data con unas cuantas propiedades, y entre ellas un array con los vídeos. La estructura de los vídeos tiene un montón de propiedades, pero nos vamos a quedar con tres de ellas en la aplicación de ejemplo: id, que es el identificador del vídeo en YouTube;  uploader, que es el nombre del usuario que subió el vídeo; y description, que es la descripción del vídeo.

El modelo de Backbone.js

El objeto que va a representar a un vídeo dentro de la aplicación de ejemplo va a ser el siguiente, que hereda del objeto Model de Backbone.js:

 var Models = {
  Video: Backbone.Model.extend(),
};

Bastante simple, ¿verdad? Una de las ventajas de Backbone.js es que si no necesitas nada más que la funcionalidad por defecto, la librería se encarga de todo lo que hay por debajo. Por lo tanto, nuestro objeto para el vídeo es bastante sencillo.

La colección de vídeos

Puesto que necesitamos cargar un conjunto de vídeos, será necesario crear también un objeto que los agrupe, por lo que definiremos un objeto que herede de Collection:

var Collections = {
  Videos: Backbone.Collection.extend({
    model: Models.Video,
    url: 'http://gdata.youtube.com/feeds/api/videos?v=2&alt=jsonc&max-results=9',
    parse: function(resp) {
      console.log('VideosCollection: Received server reponse and parsing data');
      return resp.data.items;
    },
  }),
};

La propiedad model indica a Backbone.js qué tipo de contenido va a haber en la colección de modelos, y en nuestro caso hemos definido que va a contener objetos del tipo Video.

La siguiente propiedad, url, indica la dirección en la cual va a tener acceso a los objetos. En nuestro caso indicamos que será la dirección del feed de vídeos de YouTube, con los parámetros que especifican que queremos la salida en formato JSON. Además he añadido un parámetro adicional, max-results, para que devuelva sólamente nueve resultados.

Y ahora llega la función más extraña de la colección. Por defecto Backbone.js tiene una implementación de parse(response) que devuelve directamente el resultado obtenido de la URL, lo cual va bien si los datos vienen adecuadamente organizados. En nuestro caso, si recordáis la estructura de datos mostrada anteriormente, el array de vídeos se encuentra en una propiedad interior, concretamente en response.data.items, por lo que la implementación por defecto no nos sirve y tenemos que hacer que devuelva el elemento que contiene el array de vídeos.

Las vistas de la aplicación

La aplicación mostrará una lista de vídeos, que es una vista (hereda del objeto View de Backbone.js), y cada vídeo individual será otra vista. Además, añadiremos un pequeño formulario en otra vista más, para que el usuario pueda realizar una búsqueda sobre un tema concreto y se muestren los vídeos relacionados.

La vista de un vídeo individual

El código javascript que define la vista de un vídeo es el siguiente:

SingleVideo: Backbone.View.extend({
  className: 'video',
  initialize: function() {
    this.template = _.template($('#single-video-template').val());
  },
  render: function() {
    console.log('SingleVideo: entering render');
    this.$el.html(this.template({video:this.model.toJSON()}));
    console.log('SingleVideo: leaving render');
    return this;
  },
});

Y la plantilla HTML es:

<!-- Plantilla para un vídeo individual -->
<textarea id="single-video-template" style="display:none">
  <h2>'<%= video.title %>' por <%= video.uploader %></h2>
  <div class="video-player">
    <object width="320" height="240">
      <param name="movie" value="http://www.youtube.com/v/<%= video.id %>?version=3&amp;hl=es_ES"></param>
      <param name="allowFullScreen" value="true"></param>
      <param name="allowscriptaccess" value="always"></param>
      <embed src="http://www.youtube.com/v/<%= video.id %>?version=3&amp;hl=es_ES" type="application/x-shockwave-flash" width="320" height="240" allowscriptaccess="always" allowfullscreen="true"></embed>
    </object>
  </div>
  <h3>Descripción</h3>
  <div class="video-list-description"><%= video.description %></div>
</textarea>

La propiedad className indica el contenido que se le debe dar al atributo class del elemento HTML cuando la vista sea incluída en el DOM. En este caso hemos definido una clase video que veremos más adelante.

La función initialize() es el constructor del objeto. Dentro de esta función iniciamos todos los objetos auxiliares que vayamos a necesitar dentro de la vida de esta clase. En nuestro caso tenemos una propiedad template, que contendrá la plantilla HTML para esta vista que hemos mostrado anteriormente. La función _.template es una función auxiliar de Underscore que nos permite cargar un bloque de HTML con trozos de código al estilo ERB de Ruby para interpolar variables y funciones de javascript que se pasan como parámetro, y el contenido de esta plantilla viene del contenido del elemento textarea con id single-video-template de la página.

La función render() se encarga de añadir la vista al DOM. Para ello genera el código HTML usando la plantilla definida anteriormente con el objeto que debe usar para interpolar esas variables, y lo inserta dentro del elemento this.$el. Este elemento es una caché del elemento del DOM al que está asociada esta vista, que además está extendido con todas las funciones de jQuery (de ahí lo del símbolo $).

También se puede observar que la función devuelve una referencia al objeto this puesto que, por convención, las llamadas a las funciones de un objeto de Backbone.js deberían poder encadenarse una detrás de otra.

La vista de la caja de búsqueda

El código para esta vista es:

SearchBox: Backbone.View.extend({
  events: {
    'click #search-submit': 'performSearch',
  },
  initialize: function() {
    this.template = _.template($('#search-box-template').val())
  },
  render: function() {
    console.log('SearchBox: entering render');
    this.$el.html(this.template());
    console.log('SearchBox: leaving render');
    return this;
  },
  performSearch: function() {
    console.log('SearchBox: entering performSearch');
    queryString = this.$el.find('#search-query').val();
    this.trigger('searchRequest', {queryString:queryString});
    console.log('SearchBox: leaving performSearch');
  },
});

Y su plantilla HTML es:

<!-- Plantilla para la caja de búsqueda -->
<textarea id="search-box-template" style="display:none">
  <input type="text" id="search-query">
  <button type="button" id="search-submit">Buscar</button>
</textarea>

Esta vista, al igual que la anterior, tiene unas funciones initialize() y render() que, respectivamente, inician el objeto y lo añaden al DOM de la página. La novedad en este caso es la aparición del objeto events. Este objeto define los eventos a los que va a responder la vista, que en este caso es al click sobre el elemento con id search-submit. Una vez que se pulse sobre este elemento se deberá ejecutar la función performSearch().

Y dentro de la definición de la vista se encuentra esta función, la cual obtiene la cadena de búsqueda introducida en la caja de texto (el elemento con id search-query), y lanza el evento searchRequest mediante la función de Backbone.js trigger(), pasando como parámetro dicha cadena de búsqueda. Todos los objetos de Backbone.js implementan las funciones on(), off(), y trigger(), que, respectivamente, definen un manejador para un evento, eliminan un manejador de un evento y lanzan un evento sobre la vista.

La propagación de eventos es lo que permite articular una aplicación de Backbone.js. Las vistas interiores son responsables de sus datos y notifican cambios de estado mediantes eventos que serán capturados por las vistas que las contienen:

Eventos en vistas de Backbone.js

La vista de la lista de vídeos

El código correspondiente a esta vista es:

VideosApp: Backbone.View.extend({
  initialize: function() {
    _.bindAll(this);
    this.template = _.template($('#app-template').val());
    this.searchBox = new Views.SearchBox();
    this.searchBox.on('searchRequest', this.performSearch, this);
    this.collection = new Collections.Videos();
    this.collection.on('reset', this.showVideos, this);
    this.performSearch();
  },
  render: function() {
    console.log('VideosApp: entering render');
    this.$el.html(this.template());
    this.$el.find('#video-search-box').html(this.searchBox.render().el);
    this.showVideos();
    console.log('VideosApp: leaving render');
    return this;
  },
  showVideos: function() {
    this.$el.find('#video-list-container').empty();
    var v = null;
    this.collection.each(function(item, idx) {
      v = new Views.SingleVideo({model:item});
      this.$el.find('#video-list-container').append(v.render().el);
    }, this);
    return this;
  },
  performSearch: function(evdata) {
    evdata = evdata || {};
    console.log('VideosApp: entering performSearch - queryString: ' + evdata.queryString);
    this.collection.fetch({data:{q:evdata.queryString}});
    console.log('VideosApp: leaving performSearch');
  },
});

Y la plantilla HTML es la siguiente:

<!-- Plantilla para la aplicación -->
<textarea id="app-template" style="display:none">
  <div id="video-search-box"/>
  <h1>Vídeos de YouTube con Backbone</h1>
  <div id="video-list-container"/>
</textarea>

Esta tiene varias novedades frente a la vista anterior. Puesto que esta es la vista principal de la aplicación, se debe encargar de la carga de los datos y la gestión de los eventos.

La función initialize(), igual que en el caso anterior, crea la plantilla a partir del contenido de uno de los elementos de la página. Además, inicia la subvista de la caja de búsqueda y establece un manejador para cuando esta lance el evento searchRequest. También inicia el objeto de la colección de vídeos y establece un manejador para el evento reset de la colección. Este evento se lanza cuando el objeto reinicia el conjunto de modelos, bien sea porque se han cargado los datos del servidor, o porque se ha vaciado o se han asignado los modelos a mano mediante código. Además hace una primera llamada a la función performSearch(), que veremos más adelante, para iniciar la carga de vídeos.

En la función render(), se carga la plantilla de la vista y se añade el contenido de la subvista de la caja de búsqueda dentro de su elemento correspondiente. Después se hace una llamada a la función showVideos(), que veremos a continuación, para mostrar los vídeos que contiene la colección que, como la aplicación acaba de iniciarse, en este momento se encuentra vacía.

La función showVideos() vacía el elemento que contendrá los vídeos obtenidos de la API de YouTube para, a continuación, iterar sobre la colección de vídeos y, para cada uno, instanciar una vista de vídeo individual y añadirla al elemento contenedor de la página.

La función performSearch() se encarga de realizar la llamada a la API de YouTube. Esta función será invocada cuando se capture el evento searchRequest de la vista de la caja de búsqueda, y hará una llamada a la función fetch() de la colección, pasando como parámetro el texto de la cadena de búsqueda introducida por el usuario.

El inicio de la aplicación

Lo único que queda es añadir el siguiente bloque de código:

$(document).ready(function() {
  var vs = new Views.VideosApp();
  vs.setElement($('#container')).render();
});

Este se encarga de instanciar la vista de la aplicación y añadirla al DOM de la página, dentro del elemento con id container. Y puesto que la función initialize() de la vista VideosApp se encarga de lanzar la búsqueda, en cuanto se reciban los datos se mostrarán en la página. El aspecto final de la aplicación se puede ver en esta captura de pantalla:

Captura de pantalla de la aplicación de ejemplo

No es lo más bonito del mundo, hay que reconocerlo, pero es un ejemplo de una aplicación sencilla y básica usando Backbone.js

¿Qué ha faltado?

De la funcionalidad disponible en Backbone.js, ¿qué ha faltado por ver? Muchas cosas.

No hemos visto nada de la creación y modificación de modelos y su posterior envío al servidor para ser guardados, así como tampoco hemos usado vistas complejas ni los enrutadores para gestionar los cambios de página y los fragmentos de URL.

Todo esto podría ser contenido de un futuro taller de betabeers, si os parece bien, y si no  en un futuro escribiré la continuación de este post donde añadiré algunas mejoras a esta aplicación que hagan uso de esas funcionalidades.

Espero que hayáis aprendido algo de Backbone.js y os haya resultado útil. Para más referencias, el código de este ejemplo se encuentra en mi repositorio de GitHub en esta dirección.