Modular JavaScript, AMD and RequireJS

Márton Salomváry
(Gawker Media)

budapest.js – JavaScript Meetup

12.03.2012

Concepts

  • Modular JavaScript
    • Separation of concerns
    • Interface
    • Dependencies

But no native support ☹

  • AMD = Asynchronous Module Definition
    • Transmitted over network
  • RequireJS = implementation

History

Script tag soup and global variables

1 <script src="prototype.js"></script>
2 <script src="foo.js"></script>
3 <script src="bar.js"></script>
4 <script src="baz.js"></script>

But we learned:

  • "Every time you declare a variable in the global scope, Douglas Crockford kills a kitten."
    • Name collisions
    • Overwrite/create by accident
  • Combine and minify scripts for performance

Object literals as "packages"

1 var myApp = {
2     doThis: function() {...},
3     myModule: {
4         doThat: function() {...}
5     }
6 };
  • No (less) name collisions
  • No private scope
  • Might become complicated: YAHOO.util.Dom.IEStyle.get()

Crockford's module pattern

 1 var myModule = (function() {
 2     var privateCounter = 0;
 3     function privateFunc() {...};
 4     return {
 5         increment: function() {
 6             privateCounter++;
 7         },
 8         getValue: function() {
 9             return privateCounter;
10         }
11     };
12 })();
13 
14 myModule.increment();
15 myModule.privateCounter; // won't work, it's private!

Crockford's module pattern

  • Private scope (both "properties" and "methods")
  • Clearly "declares" interface
  • Modules can be organized into "packages"
  • Singleton

The need for better modularization

OK, we combine JavaScripts, organize code into modules, but...

  • Dependencies and order defined outside the JS code:
    • Build config files
    • Special comments (see Sprockets)
  • Hard to track down dependencies.
  • Hard to implement lazy loading aka. on-demand JavaScript.

Code complexity and size is growing - we need a tool to support this.

The solutions

CommonJS modules

Create a module:

1 exports.doThis = function() {...};
2 exports.doThat = function() {...};

Use a module:

1 var myModule = require('myModule');
2 myModule.doThis();
  • Synchronous require
  • Server-oriented API, e.g NodeJS

Asynchronous Module Definition

Create a module:

1 define(['myLib'], function(myLib) {
2     return {
3         doThis: function() {...},
4         doThat: function() {...}
5     };
6 });

Use a module:

1 require(['myModule'], function(myModule) {
2     myModule.doThis();
3 });

What is RequireJS?

  • One of the AMD implementations.
  • Works in the browser and the server side too.
  • Official plugins: text, order, i18n, domReady.
  • Includes optimizer to concatenate JS and CSS files.
  • Optimizer supports NodeJS and Rhino.

Alternative implementation: curl.js.

The practice

Example project layout

  • index.html
  • js/
    • require.js
    • jquery.js
    • backbone.js
    • main.js
    • module1.js
    • module2.js
    • lib/
      • mylib.js

Create module1.js

1 define(['jquery', 'backbone', 'lib/mylib'], 
2     function($, Backbone, myLib) {
3     // initialize the module, create private state, etc..
4     return {
5         doThis: function() {...},
6         doThat: function() {...}
7     };
8 });
  • Dependencies will be passed as function arguments.
  • Return the "public interface".
  • Object (or even string) literal can be used instead of function+return.

Use a module

As a dependency in another module:

1 define(['module1'], function(module1) {
2     module1.doThis();
3     module1.doThat();
4 });

Dynamically anywhere:

1 require(['module1'], function(module1) {
2     module1.doThis();
3 });
  • Module name is inferred from file name.

Meet the HTML

Look, we eliminated the tag soup!

1 <script src="/js/require.js" data-main="/js/main"></script>

This will load js/main.js and all the dependencies.

Optimize

  • Performance best practice: combine and minify JavaScript (and CSS too).
  • But RequireJS uses separate files.

Here comes r.js, the Optimizer.

node r.js -o name=main out=main-built.js
  • Combines main.js and all its dependencies.
  • Keeps the right order.
  • Minifies.
  • Does the same with CSS.
  • Flexible build profile files (JS syntax).
  • Runs on Rhino or NodeJS.

Using third party scripts

1 require(['jquery', 
2     'order!jquery/cookie', 
3     'order!jquery/scrollto'], 
4 function() {
5     $.cookie();
6     $('.foo').scrollTo();
7 });
  • Mostly just work (use the global variable)
  • Order plugin
  • Converters
  • AMD compliant forks (eg. Backbone amdjs fork)

Integrating with web frameworks

  • Works out of the box in development
  • View helpers to render proper <script> and <link> tags
  • For production
    • "Cache-forever" preprocessing (hash or timestamp)
    • Run optimizer
    • Serve from CDN
  • RequireJS Rails
  • Work in progress for NodeJS/Express

Traps

  • Libraries that are not AMD compliant out of the box
  • Migrating legacy codebase is not trivial
  • Integration with back-end frameworks needs some work

Questions?

It's 5.7K minified, gzipped ;)

Thank you!

Márton Salomváry
(Gawker Media)

salomvary@gmail.com