Thursday, December 1, 2011

Getting Started with Backbone.js and Require.js

‹prev | My Chain | next›

With the first edition of Recipes with Backbone hot off the virtual press, I get started tonight with a little bit of follow up work. I am still not quite satisfied with the namespacing and packaging chapter for which I was responsible. There are two decent approaches, but I am holding out hope for a little better.

Tonight I plan to kick the tires on require.js. It looks as though the next version of Backbone.js is going to support Asynchronous Module Definition (AMD) compatible.

In the scripts directory of my application, I grab the necessary javascript libraries:
➜  scripts  wget http://code.jquery.com/jquery-1.7.1.js
➜  scripts  wget http://documentcloud.github.com/underscore/underscore.js
➜  scripts  wget https://github.com/jrburke/backbone/raw/optamd3/backbone.js
➜  scripts  wget http://requirejs.org/docs/release/1.0.2/comments/require.js
I am very careful here to grab the Backbone.js library from James Burke's "optamd3" branch. None of this will work with vanilla Backbone.js.

At this point, my directory layout looks like:
➜  backbone-requirejs-test  tree -I node_modules
.
├── app.js
├── index.html
├── scripts
│   ├── backbone.js
│   ├── jquery-1.7.1.js
│   ├── my-calendar.js
│   ├── require.js
│   └── underscore.js
└── styles
(app.js is just a simple, static express.js server).

The original HTML page is taken from some of the test code in Recipes with Backbone. In it, we have been loading the various Javascript files thusly:
    <script type="text/javascript" src="vendor/jquery-1.7.min.js"></script>
    <script type="text/javascript" src="vendor/underscore-min.js"></script>
    <script type="text/javascript" src="vendor/backbone-min.js"></script>

    <script type="text/javascript" src="my-calendar.js"></script>
    <script type="text/javascript">
      $(function() {
        new MyCalendar($('#paginator'));
      });
    </script>
I believe that all of that can be replaced with a single <script> tag:
    <script data-main="scripts/main" src="scripts/require.js"></script>
The scripts/require.js is just require.js. To load in my application, I am specifying (via the data-main attribute) that require.js should look for the main source in script/main.js. So now, I need to actually create that file:
require.config({
  paths: {
    'jquery': 'jquery-1.7.1'
  }
});

require(['my-calendar'], function(calendar){
  calendar.initialize();
});
When I reference 'jquery' with require.js, it will try to load it as script/jquery.js. Since I have it saved as script/jquery-1.7.1.js, I need to provide the mapping. None of the other libraries that I downloaded have version numbers in the filename so they should load without assistance.

Next, I make my first require.js function call with require(['my-calendar']). As the name suggests, that requires a single library, my-calendar.js. It then makes the contents of that library available to the anonymous function as the first and only argument, which I name calendar. In that function, I tell the calendar object to initialize() itself.

As for the my-calendar library, which is responsible for initializing my Backbone application, I need to define() it. The define() function in require.js is very similar in format to the require() function. The first argument is a list of libraries to be loaded. The second argument is a function that will be called with as many arguments as there were libraries in the first argument.

In the case of my calendar object, I need 4 libraries to initialize it: jQuery, Backbone, a Paginator view, and a Router:
define(['jquery', 'backbone', 'paginator', 'router'],
  function($, Backbone, Paginator, Router) {
    return {
      initialize: function(){
        var paginator = new Paginator({el: $('#paginator')}).render();

        new Router({paginator: paginator});
        Backbone.history.start();
      }
    };
  }
);
In there, I need to grab my "Paginator" class, instantiate and render it. Then I can pass it along to my router to actually do stuff. Both the Paginator and Router are defined similarly. The Paginator looks like:
define(['backbone', 'underscore'], function(Backbone, _) {

  _.templateSettings = {
    interpolate : /\{\{(.+?)\}\}/g
  };

  return Backbone.View.extend({
    _page: 0,
    template: _.template('<span class="prev">Previous</span> {{page}} <span class="next">Next</span>'),
    events: {
      'click .prev': 'previous',
      'click .next': 'next'
    },
    initialize: function() {
      _.bindAll(this, 'previous', 'next', 'render');
    },
    render: function() {
      console.log("[render]");
      $(this.el).html(this.template({page: this._page}));
      return this;
    },
    previous: function() {
      this.trigger('previous');
    },
    next: function() {
      this.trigger('next');
    },
    move: function(by) {
      this._page = this._page + by;
      return this;
    },
    page: function(page) {
      if (typeof(page) != "undefined") this._page = parseInt(page, 10);
      return this._page;
    }
  });
});
Amazingly, that all works. When I load up my redirection sample code, I actually see the redirection application with working pagination:

There may be something to this require.js stuff after all.


Day #122

12 comments:

  1. We we're trying to use requireJS. It is great for defining dependencies and not polluting your global namespace with objects and variables.

    Now we're facing the problem of using the optimizer included in requireJS to package our static scripts and templates with other dynamic scripts, into one file, otherwise we're having the issue of making 150+ requests to load the application when you're logged in.

    ReplyDelete
  2. Hi Chris,
    I just discovered your blog, via your Recipes book. If only I had discovered it months ago !
    We are writing a big Backbone based app at work, and are using Require.js very successfully. (Also using Qunit, jsTestDriver, and Sinon too, with Twitters BootStrap as our CSS framework).
    Just thought I'd let you know that Require and Backbone play well together. And we are using the vanilla Backbone and the vanilla jQuery, not Jame's version.

    Cheers,
    Phil,
    Melbourne, Australia

    ReplyDelete
  3. @inkel I could see where 150 request might be a problem, unless you were serving it up over SPDY, of course :P

    I'm surprised that you're having trouble with it. It took me a while to wrap my brain around requirejs, but the optimizer is quite easy to understand. I figured the ease of use for each would be roughly the same, but maybe not.

    Something for another chain post, I suppose :)

    ReplyDelete
  4. @Phil Interesting. I wonder what James' patch buys you. I would guess that it buys not having to explicitly require underscore. I wonder if that's true or if there is something else.

    Something for another chain post!

    ReplyDelete
  5. Chris, although I respect what James has done with Require (it is VERY complex under the covers), I don't like the idea of being tied to a specific version of 3rd party libs (jQuery, Backbone), that have 'special' mods done to them. So, we just gave it a shot with the current versions of those libs, and it seems to work :-)

    BTW,we don't 'require' Underscore, or Backbone. They are just bought into the page like any other lib. We then 'define' and 'require' Backbone classes. Underscore and Backbone are just available as global objects.

    ReplyDelete
  6. @Phil Oh, I definitely agree about 3rd party libraries. Based on Jeremy Ashkenas' remarks in that pull request, it sounds as though something like James' patch will be in the next Backbone. So definitely worth investigating now.

    FWIW I'm not using require-jquery, just vanilla jquery.

    I was stunned by how complex requirejs is! I hoped to read through the code some, but that's almost never ending. If there is one thing that concerns me about all of this, it is the complexity of such a vital component.

    ReplyDelete
  7. We thought about using LABS instead of Require, cause its simpler, but it doesn't have a packaging (optimiser) mechanism.
    The optimiser is giving us grief on our templates (the text!... dependancies), and we can't quite figure it out (and its nothing to do with Backbone). So, we have to post process the optimised result with a simple search and replace otherwise the code trips up in the browser.
    Cant remember all the details at the moment.
    Other than that, its working great with Backbone.

    BTW, great book you wrote with Nick. I learnt a few things.

    The other bit of tech we are using to really decouple our views is a pub/sub library like Amplify.

    Slightly off topic, here is an example of using currying and pub/sub in a backbone router to swap in and out the desired views if you are interested - https://gist.github.com/1423207

    ReplyDelete
  8. Interesting. I've used Faye for pub/sub in Backbone: http://japhr.blogspot.com/2011/09/faye-as-persistence-layer-in-backbonejs.html. I'm very partial to it because it is easy to integrate with both Ruby and Node.js on the server side. Any thoughts on what Amplify offers that Faye might not?

    Thanks for the kind words -- glad you liked the book!

    ReplyDelete
  9. Yeah! I am glad you investigated this further.

    ReplyDelete
  10. @Riebel Me too! Thanks for all the pointers. I really struggled to get this working. I would have been at a complete loss without your help :)

    ReplyDelete
  11. @Chris yeah, I would love the app to be served under SPDY, but it'll need to wait for that ;)

    Our main issue was that we have lots of modules, some shared, some others don't, regarding the user profile you're logged into. And also that the modules sometimes require usage of the text plugin and some other dynamic modules/configurations (views, data, etc). And that we're already on a short schedule, so we couldn't play too much with it.

    But we'll give it another shot in the future, that's for sure.

    ReplyDelete
  12. @Chris I have not used Faye. I'll have a look at it. Pub/Sub is so simply you could easily write your own. There are a bunch of them out there.

    ReplyDelete