Building Javascript applications have become a much more civilised thing in the later years and one of my favourite new tools is the AMD (Asynchronous Module Definition) standard, and its main script loader advocate; require.js. Using require.js lets you create javascript code organised as modules in single files with dependencies that doesn’t pollute the global scope. It also has a build tool / minifier that will let you optimize your multi-file javascript into one lean package to serve in production.
In its most basic form you can use require.js as a script loader over your existing files and have it trigger a callback once done, so loading your app.js + animations.js using require.js works like this:
<script src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.1/require.min.js"></script>
<script>
require(['app.js', 'animations.js'], function()
{
console.log('I can haz loaded js files');
});
</script>
<!-- Better yet, require.js allows you to specify a main js file to load initially -->
<script data-main="app.js" src="require.min.js"></script>
As you can see, you can kickstart your entire js environment with that last one line — and thats the advised way of doing things. It gives you a clear entry point, and from there on you should only load javascript files using the two basic calls of require.js: define() and require().
The anatomy of an AMD module is, most commonly, define(id?, dependencies?, module):
define(['dependency'], function(Dependency)
{
console.log("Dependency loaded", Dependency);
});
Lets create a small example using two modules + jquery to examine how dependencies work. Jquery, like quite a few popular libraries, registers itself as an AMD module, so I will use it in our examples. This uses three files; index.html, main.js and view.js.
- index.html
- main.js is the bootstrap file that require.js will load as soon as it is loaded itself. This will ideally handle some global setup and start the app.
- view.js is our actual view, what we can see. This file gets called from main.js and will render Hello World into #container.
<!doctype>
<html>
<body>
<div id="container"></div>
<script>
require = {
paths: {
jquery: '//ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.min'
},
baseUrl : '.'
};
</script>
<script data-main="main" src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.1/require.min.js"></script>
</body>
</html>
This last module shows how dependency free modules can be written using object literals and thus passing just 1 argument to define.
define(['jquery', 'view'], function($, View)
{
View.showSomething($('#container'));
});
With this simple example we’ve achieved a few important architectural principles you can use in building out a proper app:
- We pass dependencies, instead of abusing globals ($, I’m looking at you). This might not seem important, but in a big application keeping this tidy means less side effects.
- We have the means to keep files small and tidy, simply adding another module name to an array is a no-brainer when you add some new code.
- We have a setup that allows us to load modules based on what features are needed on a page.
Optimising network requests
But as we start building code in tiny files we take the penalty of loading that code in chunks.
require.js uses xhr requests in parallel beneath the hood, but since we need to load main.js before we detect the need for jquery + view.js we still get a scenery like this
Now if we were to add 10, 20, 50 more files and they would load in a similar fashion, how would that affect our app? Well, it wouldn’t be pretty! Fortunately for us, require.js is not built for tinker-toys but rather production web sites, and it lets you perform minifying while respecting your modular codes dependencies. You can choose to build one file to rule them all, or several files for every distinct section of your app (this lets you maintain feature dependent loading in an optimised manner).
I’ve set up a grunt.js (a really cool command line js developer tool for automated tasks) config file that will allow you to just run grunt from the projects root to build a compiled version in compiled/main.js.
You also need to change baseUrl setting in index.html for it to load from the compiled folder. In order to install grunt you must have node.js + npm installed and do npm install from the project root.
When minifying your project you have two options; You can either continue loading jquery from a CDN, or you can download it locally and have it concatenated and built into your main.js compiled file. Have a look at the grunt.js file that sets up requirejs compilation and see the two different configs:
module.exports = function(grunt) {
"use strict";
grunt.initConfig({
requirejs: {
compile: {
options: {
baseUrl: '.',
paths: {
jquery: 'jquery.min' //Use this to minifiy jquery into your main
//jquery: 'empty:' //Use this to continue using CDN loading
},
name: 'main',
out: 'compiled/main.js',
removeCombined: false
}
}
}
});
grunt.loadNpmTasks('grunt-contrib');
// Default task.
grunt.registerTask("default", "requirejs");
};
After building the project and setting baseUrl: ‘compiled’ we get a better network flow chart:
Thats it! Now we have a simple setup that is ready to be expand and built upon. You can find all source code from this post in this gist, or just git clone git://gist.github.com/4256032.git and continue building!
Where I work (Keyteq Labs, web page in Norwegian) we use require.js in all products and are very satisfied with that. I’m planning to write about some more advanced stuff we do with require.js (+ backbone.js) later on.