Aplicación de una sola página para hacer con Backbone.js

Backbone.js es un marco de JavaScript para crear aplicaciones web flexibles. Viene con modelos, colecciones, vistas, eventos, enrutador y algunas otras características excelentes. En este artículo desarrollaremos una aplicación simple de tareas pendientes que admite agregar, editar y eliminar tareas. También deberíamos poder marcar una tarea como hecho y archívalo. Para mantener la longitud razonable de esta publicación, no incluiremos ninguna comunicación con una base de datos. Todos los datos se mantendrán en el lado del cliente..

Preparar

Aquí está la estructura de archivos que usaremos:

css └── styles.css js └── colecciones └── ToDos.js └── modelos └── ToDo.js └── proveedor └── backbone.js └── jquery-1.10.2.min.js └── underscore.js └── vistas └── App.js └── index.html 

Hay pocas cosas que son obvias, como /css/styles.css y /index.html. Contienen los estilos CSS y el marcado HTML. En el contexto de Backbone.js, el modelo es un lugar donde guardamos nuestros datos. Por lo tanto, nuestros ToDos serán simplemente modelos. Y como tendremos más de una tarea, las organizaremos en una colección. La lógica de negocios se distribuye entre las vistas y el archivo principal de la aplicación., App.js. Backbone.js solo tiene una dependencia fuerte: Underscore.js. El marco también juega muy bien con jQuery, por lo que ambos van a la vendedor directorio. Todo lo que necesitamos ahora es solo un pequeño código HTML y estamos listos para comenzar.

   Mis tareas    

Como puede ver, estamos incluyendo todos los archivos JavaScript externos hacia la parte inferior, ya que es una buena práctica hacer esto al final de la etiqueta del cuerpo. También estamos preparando el bootstrapping de la aplicación. Hay un contenedor para el contenido, un menú y un título. La navegación principal es un elemento estático y no lo vamos a cambiar. Reemplazaremos el contenido del título y la div Por debajo de eso.

Planificación de la aplicación

Siempre es bueno tener un plan antes de comenzar a trabajar en algo. Backbone.js no tiene una arquitectura super estricta, que debemos seguir. Ese es uno de los beneficios del framework. Entonces, antes de comenzar con la implementación de la lógica empresarial, hablemos de la base.

Espacio de nombres

Una buena práctica es poner su código en su propio alcance. Registrar variables o funciones globales no es una buena idea. Lo que crearemos es un modelo, una colección, un enrutador y algunas vistas Backbone.js. Todos estos elementos deben vivir en un espacio privado.. App.js contendrá la clase que contiene todo.

// App.js var app = (function () var api = vistas: , modelos: , colecciones: , contenido: nulo, enrutador: nulo, todos: nulo, init: función ()  this.content = $ ("# content");, changeContent: function (el) this.content.empty (). append (el); return this;, title: function (str) $ ("h1 ") .text (str); devuélvalo;; var ViewsFactory = ; var Router = Backbone.Router.extend (); api.router = new Router (); return api;) (); 

La anterior es una implementación típica del patrón de módulo revelador. los api variable es el objeto que se devuelve y representa los métodos públicos de la clase. los puntos de vista, modelos y colecciones Las propiedades actuarán como titulares de las clases devueltas por Backbone.js. los contenido es un elemento jQuery que apunta al contenedor de la interfaz del usuario principal. Hay dos métodos de ayuda aquí. El primero actualiza ese contenedor. El segundo establece el título de la página. Luego definimos un módulo llamado VistasFactory. Entregará nuestras vistas y al final, creamos el router..

Usted puede preguntar, ¿por qué necesitamos una fábrica para las vistas? Bueno, hay algunos patrones comunes al trabajar con Backbone.js. Uno de ellos está relacionado con la creación y uso de las vistas..

var ViewClass = Backbone.View.extend (/ * logic here * /); var view = new ViewClass (); 

Es bueno inicializar las vistas solo una vez y dejarlas vivas. Una vez que se cambian los datos, normalmente llamamos a los métodos de la vista y actualizamos el contenido de su el objeto. El otro enfoque muy popular es recrear la vista completa o reemplazar el elemento DOM completo. Sin embargo, eso no es realmente bueno desde el punto de vista del rendimiento. Por lo tanto, normalmente terminamos con una clase de utilidad que crea una instancia de la vista y la devuelve cuando la necesitamos..

Definición de componentes

Tenemos un espacio de nombres, así que ahora podemos empezar a crear componentes. Así es como se ve el menú principal:

// views / menu.js app.views.menu = Backbone.View.extend (initialize: function () , render: function () ); 

Creamos una propiedad llamada menú Que mantiene la clase de la navegación. Más adelante, podemos agregar un método en el módulo de fábrica que crea una instancia del mismo..

var ViewsFactory = menu: function () if (! this.menuView) this.menuView = new api.views.menu (el: $ ("# menu"));  devuelve this.menuView; ; 

Lo anterior es cómo manejaremos todas las vistas y nos aseguraremos de obtener solo una y de la misma instancia. Esta técnica funciona bien, en la mayoría de los casos..

Fluir

El punto de entrada de la aplicación es. App.js y es en eso método. Esto es lo que llamaremos en el onload manejador de la ventana objeto.

window.onload = function () app.init ();  

Después de eso, el router definido toma el control. Basado en la URL, decide qué controlador ejecutar. En Backbone.js, no tenemos la arquitectura habitual de Model-View-Controller. Falta el controlador y la mayoría de la lógica se coloca en las vistas. Entonces, en cambio, conectamos los modelos directamente a los métodos, dentro de las vistas y obtenemos una actualización instantánea de la interfaz de usuario, una vez que los datos han cambiado..

Gestionando los datos

Lo más importante en nuestro pequeño proyecto son los datos. Nuestras tareas son lo que debemos manejar, así que empecemos desde allí. Aquí está nuestra definición de modelo.

// models / ToDo.js app.models.ToDo = Backbone.Model.extend (defaults: title: "ToDo", archivado: false, done: false); 

Sólo tres campos. El primero contiene el texto de la tarea y los otros dos son indicadores que definen el estado del registro..

Cada cosa dentro del marco es en realidad un despachador de eventos. Y debido a que el modelo se cambia con los configuradores, el marco sabe cuándo se actualizan los datos y puede notificar al resto del sistema para eso. Una vez que vincule algo a estas notificaciones, su aplicación reaccionará ante los cambios en el modelo. Esta es una característica realmente poderosa en Backbone.js.

Como dije al principio, tendremos muchos registros y los organizaremos en una colección llamada ToDos.

// colecciones / ToDos.js app.collections.ToDos = Backbone.Collection.extend (initialize: function () this.add (title: "Aprende lo básico de JavaScript"); this.add (title: "Go to backbonejs.org "); this.add (title:" Develop a Backbone application ");, model: app.models.ToDo up: function (index) if (index> 0) var tmp = this.models [index-1]; this.models [index-1] = this.models [index]; this.models [index] = tmp; this.trigger ("change");, down: function ( índice) si < this.models.length-1)  var tmp = this.models[index+1]; this.models[index+1] = this.models[index]; this.models[index] = tmp; this.trigger("change");  , archive: function(archived, index)  this.models[index].set("archived", archived); , changeStatus: function(done, index)  this.models[index].set("done", done);  ); 

los inicializar Método es el punto de entrada de la colección. En nuestro caso, agregamos algunas tareas por defecto. Por supuesto, en el mundo real, la información provendrá de una base de datos o de otro lugar. Pero para mantenerte enfocado, lo haremos manualmente. La otra cosa que es típica para las colecciones, es establecer el modelo propiedad. Le dice a la clase qué tipo de datos se almacenan. El resto de los métodos implementan lógica personalizada, relacionada con las características de nuestra aplicación.. arriba y abajo Las funciones cambian el orden de los ToDos. Para simplificar las cosas, identificaremos cada ToDo con solo un índice en la matriz de la colección. Esto significa que si queremos obtener un registro específico, debemos apuntar a su índice. Por lo tanto, el orden es simplemente cambiar los elementos en una matriz. Como puede adivinar del código anterior, esto.modelos Es la matriz de la que estamos hablando.. archivo y cambiar Estado Establecer propiedades del elemento dado. Ponemos estos métodos aquí, porque las vistas tendrán acceso a la ToDos Recopilación y no a las tareas directamente..

Además, no necesitamos crear ningún modelo desde el app.models.ToDo clase, pero tenemos que crear una instancia de la app.collections.ToDos colección.

// App.js init: function () this.content = $ ("# content"); this.todos = new api.collections.ToDos (); devuelve esto  

Mostrando nuestra primera vista (navegación principal)

Lo primero que tenemos que mostrar, es la navegación principal de la aplicación..

// views / menu.js app.views.menu = Backbone.View.extend (template: _.template ($ ("# tpl-menu"). html ()), initialize: function () this.render ();, render: function () this. $ el.html (this.template ());); 

Son solo nueve líneas de código, pero aquí ocurren muchas cosas geniales. El primero es establecer una plantilla. Si recuerdas, agregamos Underscore.js a nuestra aplicación? Vamos a utilizar su motor de plantillas, porque funciona bien y es lo suficientemente simple de usar..

_.template (templateString, [data], [settings]) 

Lo que tiene al final, es una función que acepta un objeto que contiene su información en pares clave-valor y la templateString es el marcado HTML. Ok, entonces acepta una cadena HTML, pero lo que es $ ("# tpl-menu"). html () ¿haciendo ahi? Cuando estamos desarrollando una pequeña aplicación de una sola página, normalmente colocamos las plantillas directamente en la página de esta manera:

// index.html  

Y debido a que es una etiqueta de script, no se muestra al usuario. Desde otro punto de vista, es un nodo DOM válido, por lo que podríamos obtener su contenido con jQuery. Por lo tanto, el breve fragmento de código anterior solo toma el contenido de esa etiqueta de script.

los hacer El método es realmente importante en Backbone.js. Esa es la función que muestra los datos. Normalmente, los eventos activados por los modelos se vinculan directamente a ese método. Sin embargo, para el menú principal, no necesitamos tal comportamiento..

este. $ el.html (this.template ()); 

esto. $ el es un objeto creado por el marco y cada vista lo tiene por defecto (hay un PS en frente de el porque tenemos jQuery incluido). Y por defecto, es un vacío.

. Por supuesto que puedes cambiar eso usando el nombre de etiqueta propiedad. Pero lo más importante aquí es que no estamos asignando un valor directamente a ese objeto. No lo estamos cambiando, solo estamos cambiando su contenido. Hay una gran diferencia entre la línea de arriba y la siguiente:

este. $ el = $ (this.template ()); 

El punto es que si desea ver los cambios en el navegador, debe llamar al método de renderización antes, para adjuntar la vista al DOM. De lo contrario solo se adjuntará el div vacío. También hay otro escenario donde tienes vistas anidadas. Y debido a que está cambiando la propiedad directamente, el componente principal no se actualiza. Los eventos enlazados también pueden estar rotos y debe adjuntar los oyentes nuevamente. Entonces, solo deberías cambiar el contenido de esto. $ el y no el valor de la propiedad.

La vista ya está lista y necesitamos inicializarla. Añadámoslo a nuestro módulo de fábrica:

// App.js var ViewsFactory = menu: function () if (! This.menuView) this.menuView = new api.views.menu (el: $ ("# menu"));  devuelve this.menuView; ; 

Al final simplemente llame al menú Método en el área de arranque:

// App.js init: function () this.content = $ ("# content"); this.todos = new api.collections.ToDos (); ViewsFactory.menu (); devuelve esto  

Tenga en cuenta que mientras estamos creando una nueva instancia de la clase de navegación, estamos pasando un elemento DOM ya existente $ ("# menu"). Entonces el esto. $ el propiedad dentro de la vista en realidad está apuntando a $ ("# menu").

Agregando Rutas

Backbone.js soporta el estado de empuje operaciones En otras palabras, puede manipular la URL del navegador actual y viajar entre páginas. Sin embargo, nos limitaremos a las buenas y viejas URLs de tipo hash, por ejemplo / # editar / 3.

// App.js var Router = Backbone.Router.extend (rutas: "archivo": "archivo", "nuevo": "newToDo", "edit /: index": "editToDo", "delete /: index ":" delteToDo "," ":" list ", list: function (archive) , archive: function () , newToDo: function () , editToDo: function (index) , delteToDo: función (índice) ); 

Arriba está nuestro enrutador. Hay cinco rutas definidas en un objeto hash. La clave es lo que escribirá en la barra de direcciones del navegador y el valor es la función a la que se llamará. Note que hay :índice En dos de las rutas. Esa es la sintaxis que debe utilizar si desea admitir URL dinámicas. En nuestro caso, si escribe. # edit / 3 la editToDo se ejecutará con parámetro índice = 3. La última fila contiene una cadena vacía, lo que significa que maneja la página de inicio de nuestra aplicación.

Mostrando una lista de todas las tareas

Hasta ahora lo que hemos construido es la vista principal de nuestro proyecto. Recuperará los datos de la colección y los imprimirá en la pantalla. Podríamos usar la misma vista para dos cosas: mostrar todos los ToDos activos y mostrar los que están archivados.

Antes de continuar con la implementación de la vista de lista, veamos cómo se inicializa realmente.

// en App.js visualiza la lista de fábrica: function () if (! this.listView) this.listView = new api.views.list (model: api.todos);  devuelve this.listView;  

Observe que estamos pasando en la colección. Eso es importante porque lo usaremos más tarde. Este modelo Para acceder a los datos almacenados. La fábrica devuelve nuestra vista de lista, pero el enrutador es el tipo que tiene que agregarlo a la página.

// en la lista de enrutadores de App.js: function (archive) var view = ViewsFactory.list (); api .title (archive? "Archive:": "Your ToDos:") .changeContent (view. $ el); view.setMode (archive? "archive": null) .render ();  

Por ahora, el método. lista En el router se llama sin ningún parámetro. Así que la vista no está en archivo Modo, mostrará solo los ToDos activos..

// views / list.js app.views.list = Backbone.View.extend (mode: null, events: , initialize: function () var handler = _.bind (this.render, this); este .model.bind ('change', handler); this.model.bind ('add', handler); this.model.bind ('remove', handler);, render: function () , priorityUp: function (e) , priorityDown: function (e) , archive: function (e) , changeStatus: function (e) , setMode: function (mode) this.mode = mode; devuélvalo; ); 

los modo Propiedad será utilizada durante la prestación. Si su valor es mode = "archivo" entonces solo se mostrarán los ToDos archivados. los eventos Es un objeto que llenaremos de inmediato. Ese es el lugar donde colocamos el mapeo de eventos DOM. El resto de los métodos son respuestas de la interacción del usuario y están directamente vinculados a las funciones necesarias. Por ejemplo, prioridad arriba y prioridad abajo Cambia el orden de los ToDos.. archivo Mueve el elemento al área de archivo.. cambiar Estado simplemente marca el ToDo como hecho.

Es interesante lo que está pasando dentro de la inicializar método. Anteriormente dijimos que normalmente se vincularán los cambios en el modelo (la colección en nuestro caso) a la hacer Método de la vista. Usted puede escribir this.model.bind ('cambiar', this.render). Pero muy pronto te darás cuenta de que la esta palabra clave, en el hacer El método no apuntará a la vista en sí. Eso es porque se cambia el alcance. Como solución alternativa, estamos creando un controlador con un alcance ya definido. Eso es lo que subrayó enlazar la función se utiliza para.

Y aquí está la implementación de la hacer método.

// views / list.js render: function () ) var html = '
    ', auto = esto; this.model.each (función (todo, índice) if (self.mode === "archive"? todo.get ("archivado") === verdadero: todo.get ("archivado") === falso ) var template = _.template ($ ("# tpl-list-item"). html ()); html + = template (title: todo.get ("title"), index: index, archiveLink: self .mode === "archive"? "unarchive": "archive", done: todo.get ("done")? "yes": "no", doneChecked: todo.get ("done")? 'checked = = "marcado" ': "");); html + = '
'; este. $ el.html (html); this.delegateEvents (); devuelve esto

Estamos recorriendo todos los modelos de la colección y generando una cadena HTML, que luego se inserta en el elemento DOM de la vista. Hay pocos controles que distinguen los ToDos de archivados a activos. La tarea está marcada como hecho Con la ayuda de una casilla de verificación. Por lo tanto, para indicar esto necesitamos pasar una marcado == "marcado" atribuir a ese elemento. Puede notar que estamos usando this.delegateEvents (). En nuestro caso, esto es necesario, porque estamos separando y adjuntando la vista desde el DOM. Sí, no estamos reemplazando el elemento principal, pero se eliminan los controladores de eventos. Es por eso que tenemos que decirle a Backbone.js que los vuelva a adjuntar. La plantilla utilizada en el código anterior es:

// index.html  

Observe que hay una clase CSS definida llamada hecho-sí, que pinta el ToDo con un fondo verde. Además de eso, hay un montón de enlaces que utilizaremos para implementar la funcionalidad necesaria. Todos ellos tienen atributos de datos. El nodo principal del elemento., li, tiene índice de datos. El valor de este atributo es mostrar el índice de la tarea en la colección. Observe que las expresiones especiales envueltas en <%=… %> se envían a la modelo función. Esos son los datos que se inyectan en la plantilla..

Es hora de agregar algunos eventos a la vista..

// eventos de view / list.js: 'click a [data-up]': 'priorityUp', 'click a [data-down]': 'priorityDown', 'click a [data-archive]': 'archive ',' haga clic en la entrada [estado de datos] ':' changeStatus ' 

En Backbone.js, la definición del evento es simplemente un hash. Primero escribe el nombre del evento y luego un selector. Los valores de las propiedades son en realidad métodos de la vista..

// views / list.js priorityUp: function (e) var index = parseInt (e.target.parentNode.parentNode.getAttribute ("data-index")); this.model.up (index); , priorityDown: function (e) var index = parseInt (e.target.parentNode.parentNode.getAttribute ("data-index")); this.model.down (índice); , archive: función (e) var index = parseInt (e.target.parentNode.parentNode.getAttribute ("data-index")); this.model.archive (this.mode! == "archive", index); , changeStatus: function (e) var index = parseInt (e.target.parentNode.parentNode.getAttribute ("data-index")); this.model.changeStatus (e.target.checked, índice);  

Aquí estamos usando e.target Entrando al manejador. Apunta al elemento DOM que activó el evento. Estamos obteniendo el índice del ToDo pulsado y actualizando el modelo en la colección. Con estas cuatro funciones terminamos nuestra clase y ahora los datos se muestran a la página..

Como mencionamos anteriormente, usaremos la misma vista para el Archivo página.

list: function (archive) var view = ViewsFactory.list (); api .title (archive? "Archive:": "Your ToDos:") .changeContent (view. $ el); view.setMode (archive? "archive": null) .render (); , archive: function () this.list (true);  

Arriba está el mismo controlador de ruta que antes, pero esta vez con cierto como parámetro.

Agregando y editando ToDos

Siguiendo el manual de la vista de lista, podríamos crear otro que muestre un formulario para agregar y editar tareas. Así es como se crea esta nueva clase:

// App.js / views factory form: function () if (! This.formView) this.formView = new api.views.form (model: api.todos). On ("guardado", función ( ) api.router.navigate ("", trigger: true);) devolver this.formView;  

Prácticamente lo mismo. Sin embargo, esta vez tenemos que hacer algo una vez que se envíe el formulario. Y eso es reenviar al usuario a la página de inicio. Como dije, cada objeto que extiende las clases de Backbone.js es en realidad un despachador de eventos. Hay metodos como en y desencadenar que puedes usar.

Antes de continuar con el código de vista, echemos un vistazo a la plantilla HTML:

 

Tenemos una textarea y un botón. La plantilla espera un título parámetro que debe ser una cadena vacía, si estamos agregando una nueva tarea.

// views / form.js app.views.form = Backbone.View.extend (index: false, events: 'click button': 'save', initialize: function () this.render (); , render: function (index) var template, html = $ ("# tpl-form"). html (); if (typeof index == 'undefined') this.index = falso; HTML, título: ""); ") .html (), title: this.todoForEditing.get (" title ")); this. $ el.html (template); this. $ el.find (" textarea "). focus (); this.delegateEvents (); devolver esto;, save: function (e) e.preventDefault (); var title = this. $ el.find ("textarea"). val (); if (title == "" ) alert ("Empty textarea!"); return; if (this.index! == false) this.todoForEditing.set ("title", title); else this.model.add (title: título); this.trigger ("guardado");); 

La vista es solo de 40 líneas de código, pero hace bien su trabajo. Solo hay un evento adjunto y este es el clic del botón Guardar. El método de renderización actúa de forma diferente dependiendo de la pasada. índice parámetro. Por ejemplo, si estamos editando un ToDo, pasamos el índice y obtenemos el modelo exacto. Si no, el formulario está vacío y se creará una nueva tarea. Hay varios puntos interesantes en el código de arriba. En primer lugar, en el renderizado utilizamos el .atención() Método para llevar el enfoque al formulario una vez que la vista se representa. De nuevo el delegateEvents La función debería ser llamada, porque la forma podría ser separada y adjuntada nuevamente. los salvar el método comienza con e.preventDefault (). Esto elimina el comportamiento predeterminado del botón, que en algunos casos puede estar enviando el formulario. Y al final, una vez hecho todo, activamos la salvado evento que notifica al mundo exterior que el ToDo se guarda en la colección.

Hay dos métodos para el enrutador que tenemos que completar.

// App.js newToDo: function () var view = ViewsFactory.form (); api.title ("Create new ToDo:"). changeContent (view. $ el); view.render (), editToDo: function (index) var view = ViewsFactory.form (); api.title ("Edit:"). changeContent (view. $ el); view.render (índice);  

La diferencia entre ellos es que pasamos en un índice, si el editar /: índice La ruta se empareja. Y por supuesto el título de la página se modifica en consecuencia..

Borrar un registro de la colección

Para esta característica, no necesitamos una vista. Todo el trabajo se puede hacer directamente en el controlador del enrutador.

delteToDo: function (index) api.todos.remove (api.todos.at (parseInt (index))); api.router.navigate ("", trigger: true);  

Conocemos el índice del ToDo que queremos eliminar. Hay un retirar Método en la clase de colección que acepta un objeto modelo. Al final, simplemente envíe al usuario a la página de inicio, que muestra la lista actualizada.

Conclusión

Backbone.js tiene todo lo que necesita para crear una aplicación de página única totalmente funcional. Incluso podríamos vincularlo a un servicio de back-end REST y el marco sincronizará los datos entre su aplicación y la base de datos. El enfoque basado en eventos fomenta la programación modular, junto con una buena arquitectura. Personalmente estoy usando Backbone.js para varios proyectos y funciona muy bien.