Desarrollando una aplicación escalable con Backbone.js
Spanish (Español) translation by Jonathan Samines (you can also view the original English article)
Backbone.js es una pequeña librería ( ~kb minificada ) que te permite construir aplicaciones web de página única (spa). A diferencia de muchos de sus similares, Backbone no es muy dogmático sobre la forma en que lo usas. Más allá de los conceptos básicos, el diseño de tu aplicación depende completamente de tí.
Este tutorial te ofrecerá algo de perspectiva sobre uno de los patrones populares que la comunidad ha empezado a adoptar: the Backbone Boilerplate. Usaremos éste modelo para crea una simple biblioteca de libros, la cual puedes extender fácilmente en una aplicación mucho más robusta.
Una revisión rápida sobre las librerías
Es muy flexible e increíblemente ligero.
Backbone.js es un framework Javascript que nos permite create fácilmente aplicaciones web de página única. Es muy flexible e increíblemente ligero, es por ello que se ha vuelto uno de los frameworks disponibles más populares.
Require.js es un cargador de modulos (basado en el patrón de diseño AMD) que te permite carga tus módulos Javascript y sus dependencias asíncronamente.
Underscore.js es una librería que proporciona un conjunto de funciones utilitarias que esperarías cuando trabajas con un lenguaje de programación. Entre otras cosas, te da la habilidad de iterar sobre colecciones, probar si el código es una función, y tiene un lenguaje de plantillas nativo.
¿Qué es el Backbone Boilerplate?
El Backbone Boilerplate es simplemente un conjunto de mejores prácticas y utilidades para construir aplicaciones web con Backbone. No es una librería adicional, pero integra unas pocas librerías para fomentar alguna estructura cuando se crean projectos con Backbone.
El Boilerplate de Backbone no es una librería adicional.
Exiten un par de formas para instalar el Backbone Boilerplate. El método más sencillo (y preferido) es el plugin grunt-bbb. Sin embargo, eso requiere el uso de Node.js y NPM, los cuales están fuera del alcance de éste tutorial. En su lugar haremos una instalación manual.
Para empezar, ve al repositorio de github y descarga una copia del código (deberías ver el icono .zip cerca de la parte superior de la página). La copia que estás descargando ha sido modificada del original con bastante código de ejemplo eliminado. Hay bastantes comentarios muy útiles en el código de ejemplo (del boilerplate original) - siéntete libre de leerlos en tu tiempo libre.
Eso es todo! Podemos empezar con la creación de nuestra aplicación
Tu primer modulo, El Libro!
Cuando estás trabajando con el Backbone Boilerplate (o cualquier projecto utilizando AMD/Require.js), estarás agrupando funcionalidad en módulos, y generalmente colocando cada módulo en su propio archivo. Esto crea una "separación de responsabilidades" y te permite (y a cualquier otro que lea tú código) entender fácilmente que debería estar haciendo tu código.
Para crear tu primer módulo, simplemente coloca el siguiente código en el archivo app/modules/book.js.
1 |
define([ |
2 |
"namespace", |
3 |
"use!backbone" |
4 |
], |
5 |
|
6 |
function(namespace, Backbone) {
|
7 |
|
8 |
var Book = namespace.module(); |
9 |
|
10 |
// Router |
11 |
Book.Router = Backbone.Router.extend({
|
12 |
routes: {
|
13 |
"book/:p" : "details" |
14 |
}, |
15 |
|
16 |
details: function(hash){
|
17 |
var view = new Book.Views.Details({model: Library.get(hash)});
|
18 |
view.render(function(el){
|
19 |
$("#main").html(el);
|
20 |
}); |
21 |
} |
22 |
}); |
23 |
|
24 |
// Instanciar el router |
25 |
var router = new Book.Router(); |
26 |
|
27 |
// Modelo de libro |
28 |
Book.Model = Backbone.Model.extend({});
|
29 |
|
30 |
// Colección de libros |
31 |
Book.Collection = Backbone.Collection.extend({
|
32 |
model: Book.Model |
33 |
}); |
34 |
|
35 |
// Esto obtendrá la plantilla del libro y la renderizará |
36 |
Book.Views.Details = Backbone.View.extend({
|
37 |
template: "app/templates/books/details.html", |
38 |
|
39 |
render: function(done) {
|
40 |
var view = this; |
41 |
|
42 |
// Obtener la plantilla, renderizarla al elemento de la Vista y llamar a la función done |
43 |
namespace.fetchTemplate(this.template, function(tmpl) {
|
44 |
view.el.innerHTML = tmpl(view.model.toJSON()); |
45 |
|
46 |
if (_.isFunction(done)) {
|
47 |
done(view.el); |
48 |
} |
49 |
}); |
50 |
} |
51 |
}); |
52 |
|
53 |
// Esto obtendrá la plantilla de lista de libros y la renderizará |
54 |
Book.Views.List = Backbone.View.extend({
|
55 |
template: "app/templates/books/list.html", |
56 |
|
57 |
render: function(done){
|
58 |
var view = this; |
59 |
|
60 |
namespace.fetchTemplate(this.template, function(tmpl){
|
61 |
view.el.innerHTML = tmpl({books: view.collection.toJSON()});
|
62 |
|
63 |
if (_.isFunction(done)){
|
64 |
done(view.el); |
65 |
} |
66 |
}); |
67 |
} |
68 |
}); |
69 |
|
70 |
// Requirido, devuelve el módulo compatible para AMD |
71 |
return Book; |
72 |
|
73 |
}); |
Esto podría parecer mucho, pero en realidad es simple. Dividamoslo abajo:
Definición de Modulos AMD
1 |
define([ |
2 |
"namespace", |
3 |
"use!backbone" |
4 |
], function(namespace, Backbone){
|
5 |
var Book = namespace.module(); |
6 |
|
7 |
return Book; |
8 |
}); |
Este es el formato estándar para cualquier definición de módulo AMD. Le estás diciendo al cargador de módulo que éste módulo necesita acceder a tu namespace y a backbone, los cuales están definidos en app/config.js. Dentro de la función de callback, estás registrando tu módulo, y devolviendolo al final (en conformidad con AMD).
El router del módulo
1 |
Book.Router = Backbone.Router.extend({});
|
2 |
var router = new Book.Router(); |
Siempre que el buscador es dirigido a una ruta en el hash de rutas, la función asociada es llamada. Esto es usualmente donde instancias la vista y llamas a la función de renderizado. Instanciamos el router así Backbone sabe que tiene que empezar a capturar las rutas asociadas.
Los datos del módulo
1 |
Book.Model = Backbone.Model.extend({});
|
2 |
Book.Collection = Backbone.Collection.extend({
|
3 |
model: Book.Model |
4 |
}); |
Esto es donde los datos de tu libro y la lógica de negocio es definida. Crearás nuevas instancia de tu Book.Model para almacenar cada libro y sus atributos (title, autor, etc). Book.Collection es asociado con Book.Model, y así es como representas tus modelos como entidades agrupadas. En otras palabras, una biblioteca tiene muchos libros, y una colección se parece mucho a una biblioteca.
Estos son bastante básicos, pero puedes colocar cualquier lógica de negocio en los objetos que son pasadas para extender los métodos. Si, por ejemplo, quieres crear una función que filtre libros de la colección basado en el autor, harías algo como lo siguiente:
1 |
Book.Collection = Backbone.Collection.extend({
|
2 |
model: Book.Model, |
3 |
|
4 |
filterByAuthor: function(author){
|
5 |
return this.filter(function(book){
|
6 |
return book.get('author') === author;
|
7 |
}); |
8 |
} |
9 |
}); |
"Las funciones de underscore pueden ser llamadas directamente sobre una colección de Backbone."
Esto es dependiente de la función de Underscore filter, la cual (como la mayor parte de las funciones de Underscore) pueden ser llamadas directamente sobre la colección en sí misma. Siéntete libre de leer la documentación de Backbone para más información sobre que funciones de Underscore puedes llamar en tus colecciones.
La misma idea aplica a tus modelos. Debes idealmente mandar toda tu lógica de negocio al modelo. Esto podría ser algo como agregar la habilidad a tus usuarios de configurar un libro como 'favorito'. Por ahora, puedes eliminar el método filterByAuthor de tu colección, pues no lo estaremos utilizando en éste tutorial.
Las Vistas del modelo
1 |
Book.Views.Details = Backbone.View.extend({
|
2 |
template: "app/templates/books/details.html", |
3 |
|
4 |
render: function(done) {
|
5 |
var view = this; |
6 |
|
7 |
// Obtener la plantilla, renderizarla al elemento de la vista y llamar a la función done |
8 |
namespace.fetchTemplate(this.template, function(tmpl) {
|
9 |
view.el.innerHTML = tmpl(view.model.toJSON()); |
10 |
|
11 |
if (_.isFunction(done)) {
|
12 |
done(view.el); |
13 |
} |
14 |
}); |
15 |
} |
16 |
}); |
Tu módulo contendrá múltiples vistas. En nuestro ejemplo, tenemos una vista de lista y una vista de detalle. Cada una de ellas tiene su propia plantilla, y una función de render la cual llama a fetchTemplate (definido en namespace.js), establece el resultado a las vistas innerHTML, y llama a la función asociada (done). Una cosa a notar, la vista de lista está pasando una colección a su función de plantilla, mientras la vista de detalles está pasando el modelo a su función de plantilla. En ambos casos, estamos llamando toJSON() sobre el parámetro. Esto nos ayuda a asegurar que estamos simplemente lidiando con datos a nivel de plantilla.
Plantillas, con poca o ninguna lógica
En app/templates/books/list.html
1 |
<h1>Listando los Libros</h1> |
2 |
|
3 |
<ul>
|
4 |
<% _.each(books, function(book){ %> |
5 |
<li><a href="book/<%= book.id %>"><%= book.title %></a></li> |
6 |
<% }); %> |
7 |
</ul>
|
In app/templates/books/details.html
1 |
<h1><%= title %></h1> |
2 |
|
3 |
<ul>
|
4 |
<li><b>Autor: </b><%= author %></li> |
5 |
<li><b>Año de publicación: </b><%= published %></li> |
6 |
</ul>
|
7 |
|
8 |
<a href="/">Regresar a la lista</a> |
Debido a que tenemos una vista de detalles y una vista de lista, necesitaremos una plantilla para cada una de ellas. En la vista de lista, iteraremos sobre nuestra colección y renderizaremos un enlace para cada vista de detalle del libro. En nuestro vista de detalles, mostraremos las piezas individuales de datos pertenecientes al libro en el que hemos hecho click. Somos capaces de utilizar las propiedades directamente porque estamos pasando los datos dentro de la función de plantilla con su método toJSON(), los cuales convierten los(as) modelos/colecciones a sus representaciones JSON.
¿Notas el hecho que no tuvimos que llamar preventDefault() para ninguno de los enlaces que estaba en la página? Esto es porque el código al final de app/main.js. Estamos diciendo que cualquier enalce en la página sin data-bypass="true" invocará automáticamente preventDefault(), usando nuestras rutas de Backbone en lugar del comportamiento por defecto de los enlaces.
Inicializando tus datos y configurando tus rutas por defecto
Al inicio de main.js, reemplaza el código con lo siguiente:
1 |
require([ |
2 |
"namespace", |
3 |
|
4 |
// Libs |
5 |
"jquery", |
6 |
"use!backbone", |
7 |
|
8 |
// Modules |
9 |
"modules/book" |
10 |
], |
11 |
|
12 |
function(namespace, $, Backbone, Book) {
|
13 |
window.Library = new Book.Collection([ |
14 |
{ id: 1, title: "El cuento de dos Ciudades", author: "Charles Dickens", published: 1859 },
|
15 |
{ id: 2, title: "El señor de los Anillos", author: "J. R. R. Tolkien", published: 1954 },
|
16 |
{ id: 3, title: "El Hobbit", author: "J. R. R. Tolkien", published: 1937 },
|
17 |
{ id: 4, title: "Y no quedó ninguno", author: "Agatha Christie", published: 1939 }
|
18 |
]); |
19 |
|
20 |
// Definiendo el router de la aplicación, puedes adjutar subrutas aquí. |
21 |
var Router = Backbone.Router.extend({
|
22 |
routes: {
|
23 |
"": "index" |
24 |
}, |
25 |
|
26 |
index: function(){
|
27 |
var view = new Book.Views.List({collection: Library});
|
28 |
view.render(function(el){
|
29 |
$("#main").html(el);
|
30 |
}) |
31 |
} |
32 |
}); |
33 |
|
34 |
// Todo despues del Router se mantiene igual |
35 |
}); |
Tipicamente, tu componente en el servidor pasará datos a tu aplicación Backbone a través de su API (configuras esto en tus Collecciones y Modelos). Sin embargo, en éste tutorial estamos simplemente inicializando tus vistas con unos pocos libros estáticos, y creando la ruta por defecto, la cual pasa la Biblioteca a la vista de lista de Book como su colección.
La única otra cosa que tiene que cambiar es el hecho de que estás pasando tu módulo en el cargador de módulos (nota el require) en lugar del define al principio de main.js). Haciendo esto, le estás diciendo a tu aplicación que cargue el archivo y lo pase a la función de callback por lo que tienes acceso a todas las propiedades del modelo Book.
Concluyendo
Existen otras cuantas piezas para verdaderamente tener una aplicación web escalable.
Podrías estar pensando que esto luce similar a cualquier otro tutorial de Backbone que has leído, entonces ¿qué hace a éste diferente? Bien, la clave para esto es el hecho de que toda la funcionalidad relacionada al modelo Book está almacenada en un módulo, el cual está en su propio archivo. Digamos que has decidido empezar compartiendo tu colección de películas en éste sitio. Podría ser tan simple como crear app/modules/movie.js, las plantillas asociadas, y decirle a main.js que cargue modules/movie.
Existen otras cuantas piezas para verdaderamente tener una aplicación web escalable, la más grande de las cuales es una API robusta en el servidor. Sin embargo, si recuerdas crear una separación de responsabilidades cuando trabajas con diferentes módulos en tu aplicación, la encontrarás mucho más sencilla de mantener, optimizar, y crear tu código sin llegar a muchos problemas como resultado de un tormentoso código espaguetti.
Aprende más sobre Backbone en Nettuts+
¡Sé el primero en conocer las nuevas traducciones–sigue @tutsplus_es en Twitter!



