Wednesday, December 7, 2011

Extracting Require.js Libraries from an Existing Backbone.js App

‹prev | My Chain | next›

Last night, I got under way converting my mid-sized Backbone.js application to requirejs. So far, I have a single, large Javascript file containing my entire Backbone application being loaded by require.js.

Starting with the web page, I require javascripts/main.js with require.js' data-main attribute:
<script data-main="javascripts/main" src="javascripts/require.js"></script>
In main.js, I require my main calendar.js library and use the class defined in that library to instantiate a new calendar application:
// javascripts/main.js
require(['calendar'], function(Calendar){
  var calendar = new Calendar($('#calendar'));
});
For that to work, I have converted my Backbone application to support the require.js define() function. The define() function lists the library dependencies, passing them to an anonymous callback function. In this case, I need jQuery, Underscore.js, and Backbone:
// javascripts/calendar.js
define(['jquery',
        'underscore',
        'backbone',
        'jquery-ui'],
  function($, _, Backbone) {
    return function(root_el) {

      var Models = (function() {
        var Appointment = Backbone.Model.extend({ /* ... */ });
        return {Appointment: Appointment};
      })();

      // ...
    };
  }
);
The return value is the function constructor that is instantiated in main.js.

All of that works just fine. What I would like to get started on today is extracting individual model, collection, and view classes out into individual files to be require'd via require.js.

In the absence of definitively knowing the best way to begin, I start at the top. That is, I remove the Appointment model class from javascripts/calendar.js and put it into a new file, javascripts/calendar/model/appointment.js. In that, I no longer need to assign my Backbone class to a variable. Variable assignment takes places when the library is required. Instead, I only need return the anonymous model class:
// javascripts/calendar/models/appointment.js
define(['backbone', 'underscore'], function(Backbone, _) {
  return Backbone.Model.extend({ /* ... */ });
}

With that, I need to make two changes back to my monolithic javascripts/calendar.js file. First I need to tell it that it now depends on this new javascripts/calendar/models/appointment.js library. Secondly, I need to tell my Backbone collection to use this class instead of the one that had been available as Models.Appointment internally. Fortunately, this requires just two changes:
// javascripts/calendar.js
define(['jquery',
        'underscore',
        'backbone',
        'calendar/models/appointment',
        'jquery-ui'],
  function($, _, Backbone, Appointment) {
    return function(root_el) {

      var Collections = (function() {
        var Appointments = Backbone.Collection.extend({
          model: Appointment,
          url: '/appointments',
          // ...
        });

        return {Appointments: Appointments};
      })();

      // ...
    };
  }
);
A quick sanity check in the browser reveals that everything is still working:


Something to note here is that I am only using my model in once place—the collection. If I extract my collection out into a require.js library, then the collection library can depend on the model and my application can depend on the collection. The application need never know about the model.

So, I extract my collection out into it own library:
// javascripts/calendar/collections/appointments.js
define(['backbone', 'underscore', 'calendar/models/appointment'],
       function(Backbone, _, Appointment) {
  return Backbone.Collection.extend({
    model: Appointment,
    url: '/appointments',
    // ...
  });
});
Note that it is now dependent on the Appointment model.

Back in my no-longer-quite-monolithic calendar.js, I replace the dependency on the Appointment model with a dependency on the Appointments class. I then replace the collection instantiation with the new class name:
// javascripts/calendar.js
define(['jquery',
        'underscore',
        'backbone',
        'calendar/collections/appointments',
        'jquery-ui'],
  function($, _, Backbone, Appointments) {
    return function(root_el) {
      // ...

      // Initialize the app
      var year_and_month = Helpers.to_iso8601(new Date()).substr(0,7),
          appointments = new Appointments([], {date: year_and_month}),
          application = new Views.Application({
            collection: appointments,
            el: root_el
          });
      
    };
  }
);
Unlike my model class, I believe that this class will stick around as a dependency of the calendar.js application class—it will always be the application's job to instantiate the list of appointments to be populated on my calendar.

With two classes extracted out into require.js libraries, I call it a night. Up tomorrow: Views.


Day #128

No comments:

Post a Comment