When developing large scale web applications leveraging RequireJS, at times, even the most highly cohesive of modules will require quite a few other modules as dependencies. As such, maintaining the order of these dependencies can become somewhat tedious. Fortunately, RequireJS provides a means of simplifying how modules may define dependencies for such cases.
Ordering Dependencies
If we are to consider how a typical module definition specifies dependencies, it becomes clear that one must ensure each module dependency name and it’s corresponding definition function argument have been listed in the same order:
1 2 3 4 5 6 7 8 9 | define([ 'jQuery', 'Underscore', 'Backbone' ], function($, _, Backbone){ ... } |
In the above example, jQuery, Underscore and Backbone are specified as the modules dependencies as a dependency names array passed as the first argument to define(). Once all dependencies have been loaded, the modules definition function is invoked; with each dependency passed in the same order in which they were defined in the dependency array.
From both a design and client implementation perspective, this one-to-one correlation between dependency ordering and definition function argument ordering makes perfect sense, of course, for it would obviously be extremely confusing otherwise. In general this is rarely a concern, though when a module has many dependencies it can become cumbersome.
Adding Dependencies
The necessary side effect of the dependency/argument ordering is that as other dependencies need to be added, time must be spent ordering and re-ordering dependencies if one takes care to group dependencies categorically in order to improve readability (e.g. models…, collections…, views…, etc.).
For example, consider the following:
1 2 3 4 5 6 7 8 9 10 11 | define([ 'jQuery', 'Underscore', 'Backbone', 'SomeModel', 'SomeCollection' ], function($, _, Backbone, SomeModel, SomeCollection){ ... } |
If we were to decide that this module also needed, say, Handlebars, we could simply add the new dependency to the end of the dependencies array, and then just add it to the end of the factory function’s arguments as follows:
1 2 3 4 5 6 7 8 9 10 11 12 | define([ 'jQuery', 'Underscore', 'Backbone', 'SomeModel', 'SomeCollection', 'Handlebars' ], function($, _, Backbone, SomeModel, SomeCollection, Handlebars){ ... }); |
While the above approach will certainly work, it fails to aid in readability as Handlebars is grouped with the application specific dependencies – the Model and Collection, as opposed to being grouped with the module’s framework dependencies. This may seem like a trivial detail, however, considering code is typically read many more times than it is written, it makes sense to organize dependencies as they are added in order to save ourselves and others time in the future when viewing the dependencies.
And so, ideally a team would have an established pattern of grouping dependencies in some kind of logical order. For example, framework specific dependencies could be listed first, followed by application specific dependencies etc. This ordering could be as simple or complex as a team collectively decides, though I would recommend keeping it generally simple.
With this in mind we could improve the above example as follows:
1 2 3 4 5 6 7 8 9 10 11 12 | define([ 'jQuery', 'Underscore', 'Backbone', 'Handlebars', 'SomeModel', 'SomeCollection' ], function($, _, Backbone, Handlebars, SomeModel, SomeCollection){ ... }); |
Organizing Dependencies
If we are to consider the above example as being somewhat typical, then it becomes rather clear that with each new dependency added we will likely have to repeat the ordering process. Again, while this may seem insignificant, it can easily lead to exceptions being thrown if any dependencies are out of order.
Fortunately, RequireJS provides a simplified CommonJS wrapping implementation, or Sugar syntax, which can be used to solve such issues. This sytax (which will feel natural to those who use Node) allows one to simply provide a module’s definition function to the module’s define call, and specifiy require as a single argument, as follows:
1 2 3 4 5 | define( function(require){ ... }); |
Using this pattern, we can refactor the above example to be more easily managed as follows:
1 2 3 4 5 6 7 8 9 10 11 | define( function( require ) { var $ = require('jQuery'), _ = require('Underscore'), Backbone = require('Backbone'), Handlebars = require('Handlebars'), SomeModel = require('SomeModel'), SomeCollection = require('SomeCollection'); ... }); |
With this pattern of dependency mapping it becomes much easier to add and remove dependencies as needed, with the added benefit of reading much more naturally. This pattern also feels more familiar as it is similar to import directives in other languages.
Conclusion
Managing module dependencies in RequireJS is quite simple and becomes even simpler when leveraging the Sugar syntax described above. When doing so, it is important to keep in mind that this syntax relies on Function.prototype.toString(), which, while having good support in most modern browsers, does not provide predictable results in certain older browsers. However, as the documentation states, using an optimizer to normalize dependencies – such as the very powerful RequireJS Optimizer – will ensure this approach works across all browsers.
As a general rule of thumb, I typically use the Sugar syntax approach when there are more than 4-5 dependencies and have found it has simplified managing dependencies in modules rather nicely.