Organizar nuestros proyectos con BackboneJS y RequireJS

Requirejs

Cada vez es más importante modularizar nuestra parte frontal de los proyectos. La complejidad y la aparición de nuevas tecnologías obliga a los desarrolladores a utilizar herramientas que hace unos años era impensado que existirían.
Hoy veremos cómo organizar nuestros proyectos usando dos tecnologías muy conocidas: RequireJS y BackboneJS.

¿Qué es AMD?
AMD son las siglas de Asynchronous Module Definitions, nos permiten cargar ficheros Javascript asincrónicamente, ganando en rendimiento y tiempo de carga de nuestra aplicación.

¿Qué es RequireJS?
RequireJS nos permite modularizar nuestros archivos Javascript, añadiendo dependencias entre ellos. La carga de los mismos se hace de forma asíncrona, aparte de que pueden tener dependencia entre ellos. Lo que ganamos con esto es solo utilizar el código JS que necesitemos, evitando cargar cientos de líneas en una página que quizás no las requiera.
Actualmente tiene la mejor comunidad entre todos los módulos AMD.

Estructurando nuestro proyecto
Tomemos el siguiente ejemplo de estructura de ficheros para ver la potencia de Backbone junto con RequireJS

├── imgs
├── css
│   └── style.css
├── templates
│   ├── projects
│   │   ├── list.html
│   │   └── edit.html
│   └── users
│       ├── list.html
│       └── edit.html
├── js
│   ├── libs
│   │   ├── jquery
│   │   │   ├── jquery.min.js
│   │   ├── backbone
│   │   │   ├── backbone.min.js
│   │   └── underscore
│   │   │   ├── underscore.min.js
│   ├── models
│   │   ├── users.js
│   │   └── projects.js
│   ├── collections
│   │   ├── users.js
│   │   └── projects.js
│   ├── views
│   │   ├── projects
│   │   │   ├── list.js
│   │   │   └── edit.js
│   │   └── users
│   │       ├── list.js
│   │       └── edit.js
│   ├── router.js
│   ├── app.js
│   ├── main.js  // Bootstrap
│   ├── order.js //Require.js plugin
│   └── text.js  //Require.js plugin
└── index.html

Usando RequireJS para llamar asincrónicamente nuestros ficheros
Un uso básico de RequireJS incluye el siguiente código HTML para nuestro archivo index.html

<!doctype html>
<html lang="en">
<head>
    <title>Example</title>
    <!-- Load the script "js/main.js" as our entry point -->
    <script data-main="js/main" src="js/libs/require/require.js"></script>
</head>
<body>

<div id="container">
  <div id="menu"></div>
  <div id="content"></div>
</div>

</body>
</html>

Nuestro fichero main.js contendrá lo siguiente:

require.config({
  paths: {
    jquery: 'libs/jquery/jquery',
    underscore: 'libs/underscore/underscore',
    backbone: 'libs/backbone/backbone'
  }

});

require([

  // Load our app module and pass it to our definition function
  'app',
], function(App){
  // The "app" dependency is passed in as "App"
  App.initialize();
});

La variable paths contienen alias de las rutas a nuestras librerías. Veremos abajo un ejemplo de un módulo creado con RequireJS:

define([
  // Alias definidos antes
  'jquery',     // lib/jquery/jquery
  'underscore', // lib/underscore/underscore
  'backbone'    // lib/backbone/backbone
], function($, _, Backbone){
  // Las variables de jQuery, Underscore y Backbone disponibles para usar.
  return {};
});

El fichero App.js debe ser pequeño para que su carga sea rápida. Al ser asincrónico lo demás se cargará junto con la página:

define([
  'jquery',
  'underscore',
  'backbone',
  'router', // Request router.js
], function($, _, Backbone, Router){
  var initialize = function(){
    Router.initialize();
  }

  return {
    initialize: initialize
  };
});

Nuestro router.js

define([
  'jquery',
  'underscore',
  'backbone',
  'views/projects/list',
  'views/users/list'
], function($, _, Backbone, Session, ProjectListView, UserListView){
  var AppRouter = Backbone.Router.extend({
    routes: {
      // Algunas urls de ejemplo
      '/projectos': 'showProjects',
      '/users': 'showUsers',

      // Default
      '*actions': 'defaultAction'
    }
  });

  var initialize = function(){
    var app_router = new AppRouter;
    app_router.on('showProjects', function(){
      var projectListView = new ProjectListView();
      projectListView.render();
    });
    app_router.on('showUsers', function(){
      var userListView = new UserListView();
      userListView.render();
    });
    app_router.on('defaultAction', function(actions){
      console.log('No route:', actions);
    });
    Backbone.history.start();
  };
  return {
    initialize: initialize
  };
});

Modularizando una vista de Backbone
Nuestro fichero views/project/list Interactuará con el DOM de la siguiente manera:

define([
  'jquery',
  'underscore',
  'backbone',
  'text!templates/project/list.html'
], function($, _, Backbone, projectListTemplate){
  var ProjectListView = Backbone.View.extend({
    el: $('#container'),
    render: function(){
      var data = {};
      var compiledTemplate = _.template( projectListTemplate, data );
      this.$el.append( compiledTemplate );
    }
  });
  return ProjectListView;
});

Modularizando una colección, modelo y vista
Primero definimos un modelo:

define([
  'underscore',
  'backbone'
], function(_, Backbone){
  var ProjectModel = Backbone.Model.extend({
    defaults: {
      name: "Nombre de ejemplo"
    }
  });
  return ProjectModel;
});

Luego nuestra colección:

define([
  'underscore',
  'backbone',
  'models/project'
], function(_, Backbone, ProjectModel){
  var ProjectCollection = Backbone.Collection.extend({
    model: ProjectModel
  });
  return ProjectCollection;
});

Por último, definimos una vista que interactúe con nuestra colección:

define([
  'jquery',
  'underscore',
  'backbone',
  'collections/projects',
  'text!templates/projects/list.html'
], function($, _, Backbone, ProjectsCollection, projectsListTemplate){
  var ProjectListView = Backbone.View.extend({
    el: $("#container"),
    initialize: function(){
      this.collection = new ProjectsCollection();
      this.collection.add({ name: "Example"});
      var compiledTemplate = _.template( projectsListTemplate, { projects: this.collection.models } );
      this.$el.html(compiledTemplate);
    }
  });
  return ProjectListView;
});