Patrón de diseño MVC del lado cliente con Backbone.js



En el apasionante mundo del desarrollo web, han ido apareciendo diferentes frameworks y utilidades para hacer del desarrollo web una experiencia más gratificante y sencilla. Existen frameworks para multitud de lenguajes y técnicas de programación.

Por ejemplo, en el mundo de Ruby tenemos el famosísimo y muy usado Rails que implementa varios patrones y principios de diseño como el patrón MVC (Model View Controller) tan necesario en el desarrollo web para crear aplicaciones fácilmente mantenibles y el principio DRY (Dont Repeat Yourself) que ayuda a su vez a mejorar la mantenibilidad y sencillez de nuestro código al ser necesario realizar cambios en un único lugar de nuestro código si lo seguimos.

En diferentes lenguajes existen otros frameworks de renombre que implementan a su vez esos mismos principios y patrones. Symfony para PHP, Django para Python, Grails para Java/Spring/Groovy. A su vez, Symfony, Rails y Grails implementan el paradigma de programación CoC (Convention over Configuration) en el que se intenta que el programador tome las mínimas decisiones posibles para ganar en simplicidad sin perder flexibilidad de manera forzosa.


Estas son poderosas herramientas a disposición de los programadores que hacen uso de ellas para construir sitios y aplicaciones web que de otra manera sería complicado construir. Pero en el mundo del JavaScript (y del CSS) eso no ocurría así, hasta hace poco. Hoy vamos a hablar de la implementación del patrón de diseño MVC del lado cliente con Backbone.js

El mundo del desarrollo JavaScript

A pesar de que existen frameworks JavaScript como jQuery, Dojo, Scriptaculous o MooTools, ninguno de ellos se centra en la forma en la que escribimos código para el lado del cliente. Utilicemos la librería que utilicemos, lo común es tener un montón de código JavaScript spaghetti que después minimizamos en un archivo (o varios) para servir en nuestro sitio web. Esto a la larga se convierte en un infierno de selectores y callbacks que tenemos que mantener sincronizados con el código HTML para que sigan funcionando como se espera.

Backbone.js implementa el patrón de diseño MVC en el lado del cliente, pero no solo nos ofrece eso, es complicado enumerar la cantidad de beneficios que Backbone.js aporta a nuestros proyectos sin probarlo primero, no solo aporta una forma de estructurar nuestro contenido sino que nos ofrece infinidad de funciones, interfaces y utilidades que facilitan de forma ostentosa nuestra tarea.

Implementación del paradigma MVC en Backbone.js

Backbone.js implementa cuatro clases:

  • Model

  • View

  • Controller o Router en versiones superiores a la 0.5.0

  • Collection


El concepto principal que hay que seguir cuando se usa Backbone.js es conseguir que las vistas escuchen cambios en el modelo y reaccionen consecuentemente a esos cambios por si mismas. Si comparamos la estructura de Backbone con un framework de la parte servidora como Rails las cosas cuadrarían más o menos así:

  • Backbone.Model: Como los modelos en Rails sin métodos de clase. Envuelve un resgistro de datos en la lógica de negocio

  • Backbone.Collection: Un grupo o colección de modelos en la parte cliente que implementa lógica para ordenación filtrado y adición

  • Backbone.Router/Backbone.Controller: Como routes.rb en Rails y acciones de controlador. Mapea URLs con funciones

  • Backbone.View: Una pieza de interfaz de usuario (UI) lógica y reutilizable. Suele estar asociada a un modelo pero no tiene por que hacerlo forzosamente

Model

Un modelo puede significar cosas diferentes en diferentes implementaciones de MVC. En Backbone, un modelo representa una entidad singular que contiene los datos además de gran parte de la lógica que los rodea. Por lo tanto, un modelo nos proporciona una forma de leer y escribir propiedades y atributos de manera arbitraria en un conjunto de datos. ¿El qué?. No te preocupes, aquí va el primer ejemplo:

Editor = Backbone.Model.extend({
    initialize: function() {
        alert("Soy un editor en Genbeta Dev!");
    },
    defaults: {
        name: 'Oscar Campos',
        memberSince: '11 Junio 2011'
    }
});
La función initialize es disparada cuando creamos una nueva instancia del tipo Editor. En el ejemplo solo muestra un mensaje de alerta, pero en una aplicación real debería de inicializar de forma correcta el objeto. Además hemos definido unos datos por defecto para rellenar el objeto en caso de que no se le pasen parámetros en su construcción.

Fijando los Atributos

Podemos fijar atributos de varias maneras. Bien de forma explicita en la construcción de la instancia, o bien a posteriori:

// Definimos atributos en la construcción
var editor = new Editor({ name: "Carlos Paramio", memberSince: '18 de Marzo del 2011' });
// Definimos atributos a posteriori
var editor2 = new Editor();
editor2.set({ name: 'Fernando Siles', memberSince: '5 de Abril del 20110 });
Utilizar los parámetros en el constructor o utilizando el método set tiene el mismo efecto.

Obteniendo los Atributos

Ya hemos fijado los atributos, ahora vamos a obtenerlos:

var editorName = editor.get("name");
Esto no tiene misterio alguno, el método anterior fijaría la variable editorName a ‘Carlos Paramio’.

Métodos de manipulación de Atributos

Los modelos pueden contener todos los métodos que consideremos necesarios para manipular los atributos. Los métodos son todos públicos por defecto:

Editor = Backbone.Model.extend({
    initialize: function() {
        alert("Soy un editor en Genbeta Dev!");
    },
    defaults: {
        name: 'Oscar Campos',
        memberSince: '11 Junio 2011',
        posts: []
    },
    addPost: function(newPostUrl) {
        var posts_array = this.get("posts");
        posts_array.push(newPostUrl);
        this.set({ posts: posts_array });
    }
});
var editor = new Editor(
    { 
        name: "Carlos Paramio", 
        memberSince: "18 de Marzo del 2011", 
        posts: ["/bases-de-datos/una-introduccion-a-mongodb"]
    }
);
editor.addPost("/gestores-de-contenido/refinery-cms-alcanza-la-release-10");
var posts = editor.get("posts");
La variable posts contendría ['/bases-de-datos/una-introduccion-a-mongodb', '/gestores-de-contenido/refinery-cms-alcanza-la-release-10']

Escuchando cambios en el modelo

La parte más excitante e importante sin duda de toda la librería. Todos los atributos de un modelo pueden tener listeners asociados a ellos que detectan cambios en sus valores. Vamos a ver un concepto de como se utiliza esto utilizando el método initialize para asociar los eventos a nuestro modelo:

Editor = Backbone.Model.extend({
    initialize: function() {
        alert("Soy un editor en Genbeta Dev!");
        this.bind("change:name", function() {
            var name = this.get("name");
            alert("Nombre de editor cambiado a " + name);
        });
    },
    defaults: {
        name: 'Oscar Campos',
        memberSince: '11 Junio 2011',
        posts: []
    },
    changeName: function(name) {
        this.set({ name: name });
    }
});
var editor = new Editor({ name: "Carlos Paramio", memberSince: "18 de Marzo del 2011" });
editor.changeName('Carlos');
El método changeName disparará el evento y la función alert será invocada. Si queremos asociar el listener change a cualquier atributo del objeto y no solo a name, podemos hacerlo de forma sencilla de esta manera this.bind("change", function() { ... }); y así capturará los cambios en cualquier atributo del modelo.

En este punto, ya deberías de apreciar los beneficios de utilizar Backbone.js en tus proyectos. Además cabe destacar que puedes utilizar Backbone junto a cualquier otra librería como jQuery o MooTools por ejemplo.

Collection

Una colección en Backbone es sencillamente una colección de modelos. Podríamos decir que las colecciones son el análogo al resultado (por ejemplo) de una consulta SQL que devuelve varios registros. Las colecciones pueden definirse de la siguiente manera:

var EditoresCollection = Backbone.Collection.extend({
    model: Editor
});
Lo más destacable del anterior ejemplo es que hemos definido de que tipo de modelo es nuestra colección, en este caso, es una colección de Editores. A partir de ahí podemos jugar todo lo que queramos añadiendo métodos que faciliten la tarea de trabajar con colecciones.

Operaciones CRUD

Las colecciones soportan las típicas operaciones CRUD a las que estamos acostumbrados de forma sencilla:

// Create (add)
var editores = new EditoresCollection;
editores.add([
    {name: 'Carlos Paramio', memberSince: '18 de Marzo del 2011' },
    {name: 'Fernando Siles', memverSince: '5 de Abril del 2011' }
]);
// Read
var fernando = editores.get(1);
// Update
editores.at(0).set({posts: ['/un-post/super-chulo']});
// Delete
editores.remove(fernando);
Las colecciones también emiten eventos que pueden ser capturados por listeners para actuar en consecuencia cuando el contenido de la colección es modificado.

Las colecciones disponen de infinidad de métodos para ordenar, convertir a JSON y muchas cosas más, te recomiendo que le eches un vistazo a la documentación del proyecto para conocer más acerca de ello en detalle.

View

Las vistas en Backbone difieren un poco de lo que los puristas del MVC conciben como “view“ puesto que podrían definirse incluso como controladores. Digamos que en Backbone, las vistas se usan para reflejar como es el aspecto de nuestro modelo de datos. También se encargan de capturar eventos y reaccionar de forma adecuada.

Las vistas no tienen nada que decir sobre nuestro HTML o CSS y pueden ser usadas con cualquier librería de templates en JavaScript. La idea global es organizar nuestra interfaz en vistas lógicas apoyadas por modelos que pueden ser actualizadas de forma independiente cuando cambia el modelo.

En lugar de buscar en un objeto JSON un elemento del DOM para actualizar el HTML a mano, podemos asociar el método render de la vista al evento change del modelo y así mantener los datos mostrados en la interfaz de usuario actualizados.

Backbone.js usa y requiere Underscore.js que incluye una librería de templates. Podemos usar esa librería o la de cualquier otra herramienta JavaScript que incluya templates o no usar templates para nada.

La propiedad “el”

La propiedad el es una referencia al objeto DOM creado/usado en el documento HTML para la vista. Todas las vistas tienen una propiedad el, y si no está definida, Backbone.js construirá una por si mismo, un elemento div vacío.

Se puede asignar la propiedad el directamente a un elemento que ya existe en el DOM. También puede (y debe) definirse un nombre de clase y un id para el elemento en caso de que no exista y tengamos la intención de crearlo nosotros mismos. En caso de crear el elemento podemos usar la propiedad tagName en lugar de el

// Crear una vista y un elemento para la vista desde cero
var EditorRow = Backbone.View.extend({
    tagName: "li",
    className: "editor-row",
    render: function() {
        ...
    }
});
// Crear una vista usando un elemento existente en el DOM
var editorRowExists = Backbone.View.extend({
    el: $("#editor-row-container"),
    render: function() {
        ...
    }
});

Render

Todas las vistas deben sobre escribir al método render puesto que es un método vacío. Obviamente, el código del método render debe renderizar la plantilla de la vista con los datos del modelo y actualizar la propiedad el con el nuevo código HTML. ¿Mande lo qué?. Un ejemplillo para clarificar esto un poco:

var editorRow ) BAckbone.View.extend({
    render: function() {
        $(this.el).html(this.model.get("name"));
        return this;
    }
});
Es conveniente devolver el objeto this para poder hacer llamadas encadenadas. Backbone es neutral con respecto a la forma de presentar el HTML al navegador, usa aquella librería de templates con la que más cómodo te sientas, o no uses ninguna, todo depende de tu forma de programar y lo que uses.

Eventos

Toda vista pude definir eventos y sus listeners:

var editorRow = Backbone.View.extend({
    tagName: "div",
    className: "editor-row",
    events: {
        "click .name": "handleClick"
    },
    ...
    handleClick: function() {
        alert("Has hecho click en el nombre del editor, ¿quieres un pin?");
    }
});
Sencillamente definimos un evento y un callback, si has trabajado con eventos anteriormente, debe resultarte muy familiar y sencillo. También podemos asociar eventos a modelos y colecciones:
var editorRow = Backbone.View.extend({
    tagName: "div",
    className: "editor-row",
    initialize: function (args) {
        this.model.bind('change:name', this.changeName);
        this.collection.bind('change:name', this.changeCollectionName);
    }
});

Controller/Router

Los controladores (< = 0.5.0) o routers (>=0.5.1) se usan para enrutar las URLs de nuestras aplicaciones cuando se usan hash tags (#). Es muy similar al enrutamiento de frameworks MVC de la parte servidora.

Las rutas deben de contener siempre al menos una ruta y una función con la que estar mapeada. Las rutas interpretan cualquier cosa después del “#” en la url, por ejemplo https://www.genbetadev.com/#/editores/ayuda. Seguramente os suene de Twitter:

var WorkSpace = Backbone.Router.extend({
    routes: {
        "!/":                                    "root",
        "!/editores"                           "editores"
    },
    root: function() {
        ...
    },
    editores: function() {
        ...
    }
});
Si quieres que al utilizar el boton “atrás” del navegador todo vaya sobre ruedas, debes asegurarte de invocar el método start() del objeto history de Backbone después de crear la instancia de tu controlador/enrutamiento:
var AppRoutes = new AppRoutes();
Backbone.history.start();

Enrutado Dinámico

En muchas ocasiones necesitaremos hacer uso de enrutado dinámico, Backbone nos ofrece por supuesto una manera sencilla de utilizar y mezclar enrutado simple y enrutado dinámico para acceder a URLs como esta: https://www.genbetadev.com/#/posts/345. Esto es extremadamente útil y es muy sencillo de implementar:

var AppRoutes = Backbone.Router.extend({
    routes: {
        "/posts/:id":            "getPost",
        ...
    },
    getPost: function(id) {
        alert("Has solicitado el post " + id);
    },
    ...
});

Conclusión

Hemos visto como implementar el patrón de diseño MVC del lado cliente con Backbone.js utilizándolo en conjunción con cualquier otra librería o framework JavasCript como jQuery por ejemplo. Esta dinámica nos permite estructurar nuestro código JavaScript de manera eficiente para no escribir código spaghetti cada vez menos mantenible.

Los que usamos ExtJS de manera habitual, estamos muy acostumbrados a este tipo de patrón y comportamiento puesto que los stores de ExtJS se comportan como los modelos de Backbone y mucho más ahora que ExtJS 4 permite la definición de modelos en la parte cliente. Aún así, recomiendo el uso de Backbone.js para escribir código JavaScript mantenible y estructurado.



Más Información | Página oficial de Backbone.js

Portada de Genbeta