Getting started with modular JavaScript
If you are developing rich internet application, and moving business logic to the client-side by truckloads, there is nothing better than using some form of module system, a loader that can take care of dependencies and asynchronous loading of your modules, and a system to package everything up for production use as a minified single JavaScript file. This is a very quick introduction to RequireJS and AMD modules which makes all of above come true.
In this tutorial, you will find just one way to do things. There are many more, but as soon as you figure out this one way of doing things, you will most certainly discover the others with ease. Therefore, we won't dwell on all the nuances of AMD modules, and instead focus on how I generally do things so you can get started quickly and painlessly.
NodeJS (we'll need it later)
Before you start, you will need to install NodeJS in your computer. Installation is well covered on the homepage, so I won't go into details, but keep in mind that, if you use Linux, you probably do not want to install the version from your distribution's package repository unless you are running Arch Linux and use AUR to do it. Grab the source and compile it (trust me, it's not hard). For Linux users, here's a copy-paste series of commands that will install NodeJS 0.6.15 into your user's home directory (note it needs to be all one line, so I had to break it up with ):
cd ~ wget http://nodejs.org/dist/v0.6.15/node-v0.6.15.tar.gz tar xvf node-v0.6.15.tar.gz mkdir -p ~/local cd node-v0.6.15 ./configure --prefix=$HOME/local make install echo 'export PATH=$PATH:$HOME/local/bin' >> ~/.bashrc echo 'export NODE_PATH=$HOME/local' >> ~/.bashrc source ~/.bashrc
Now you can type node -v and confirm NodeJS is working. Also make sure NPM is installed along with NodeJS by typing npm -v.
Apart from NodeJS, you will also need to create a project directory. Let's call our project test (yes, very creative, I know).
mkdir test cd test mkdir js mkdir css mkdir img
There's our project tree.
Now we need a few more things. First, an index file that looks like this:
<!doctype html> <html> <head> <title>RIA test</title> <link rel="stylesheet" href="css/main.css"> <script src="js/require.js" data-main="js/boot"></script> </head> <body> <!-- move along, nothing to see here! --> </body> </html>
This file will remain absolutely intact throughout the development. It does mention a few files and paths that we currently don't have, so let's take care of those.
Create an empty CSS file:
Download require.js and jquery.js, and create empty boot.js:
cd js wget http://requirejs.org/docs/release/1.0.7/comments/require.js wget http://code.jquery.com/jquery-1.7.2.js touch boot.js cd ..
And... that's it. That's the skeleton setup for our RIA frontend. If you haven't bothered to create these, you can grab a zip file with the contents we have so far.
Now, a word about what these files will do.
require.js is our module loader with dependency resolution. It has an API that conforms to the AMD spcification which we will make use of to build our modules.
jquery.js... ok, seriously, does anyone need an explanation about what jQuery is and what it is used for?
boot.js is our main module, which kicks off the whole application (more on that later).
main.css is a CSS file that we will use to collect all our CSS modules (yes, you read right, CSS modules).
Ok, let's get the ball rolling.
If you load the index.html now, you'll see that nothing actually happens, and our page is blank. That's because our boot module isn't doing anything yet. So let's add something to it.
Before we do this, you should be aware that boot module is special, not just because it's the last module that gets loaded, but also because... Um, what's that? Yes, I said 'last'. It's last because any dependencies that it lists will get loaded first. And dependencies of those dependencies before that... And so on. Got it? Ok, where was I...
boot.js uses a syntax that is a wee bit different than other modules. I'll get around to that, but first, let me show you the code:
require(['jquery'], function($) { $(document).ready(function() { alert('ready!'); }); });
If the code is correct, you should see no JavaScript errors, and you should see an alert box that says ready!. What that means is, jquery was successfully loaded, and the ready even handler was fired correctly as well. So everything works. (Get the zip file of this step.)
The whole module is basically a require() call with two arguments. The first argument is a list of dependencies. You list dependencies without the .js extension, and relative to wherever you boot script is. So if your boot script is in js/ directory (as it should be per this tutorial), then a dependency that looks like lib/somemodule.js will be loaded from js/lib/somemodule.js. Although paths are relative, true relative paths cannot be used. So, you cannot say something like ../lib/somemodule.js or ./lib/somemodule.js. There are workarounds for this, but those workarounds are neither elegant, nor strictly required (no pun intended) to have a good experience developing with AMD.
Let's build our first AMD modules
First order of business is templating. We want to make a very simple template library, and use it as a module.
Create a directory called lib in the js directory. That's where we will keep all our helper libraries:
mkdir js/lib touch js/lib/template.js
Let's edit the template.js now:
define([], function() { });
First thing first, that's the module skeleton. It looks very much like boot.js, except that it uses define() call instead of require() call. And that's basically your AMD module... more or less.
The empty array is our dependency list, which is empty because we have (and need) no dependencies. The function is called a factory function, and its job is to return the finished module.
The module is currently empty, because factory function doesn't do anything. Since it has no code in it, it's not very useful (not useful at all, that is), so let's work on that:
define([], function() { var template = {}; return template; });
Ok, that's better. What we've done is, we've created an empty object called template, and returned it. Now our template module is actually a real module, albeit it still does nothing.
The template object actually represents the eternal interface of our module. Anything that we put in that object will be exposed (because the object is returned from the function). This is where we define the API for the module.
The template module obviously needs a rendering function. So let's expose such a function through the template object:
/** * Render template * * @param {String} t Template * @param {Object} d Data * @return {String} Rendered template */ template.render = function(t, d) { // For each key in `d`, subsitute key in template with value for (key in d) { // We'll use '$key' format for placeholders in our template t = t.replace(new RegExp('\\$' + key, 'g'), d[key]); } // Return 'rendered' template return t; };
This is rudimentary template 'engine' that simply takes an object with keys, and replaces $+key placeholders in a string with values corresponding to those keys. Let's test this out. Edit the boot module to look like this:
require(['jquery', 'lib/template'], function($, template) { $(document).ready(function() { var tHithere = "Hi there. It's now $time"; $('body').append(template.render(tHithere, {time: new Date()})); }); });
If you reload the page now, it should print out "Hi there, it's now NOW", where NOW is your current date and time were you are (in my case it's "Hi there, it's now Thu Apr 12 2012 17:26:35 GMT+0200 (CEST)"). (zip file of current progress)
So, let me show you something interesting now. Open your development console and try rendering a template. For example:
> template.render('My name is $name', {name: 'Branko'});
What you get is a ReferenceError. Why? Because modules are not global. Whatever you defined in your module is not accessible globally, and even when the module is referenced from another module, it's only accessible from within that module. This makes it possible to have complete freedom when defining stuff inside your modules.
So, module exposed methods are accessible from modules that reference (load) the module. How about if we wanted to keep some things private? Let's do that now. The line where we build the regular expression is a bit ugly, so let's move that part out into a private function:
define([], function() { var template = {}; // Build regular expression function keyRe(key) { return new RegExp('\\$' + key, 'g'); } /** * Render template * * @param {String} t Template * @param {Object} d Data * @return {String} Rendered template */ template.render = function(t, d) { // For each key in `d`, subsitute key in template with value for (key in d) { // We'll use '$key' format for placeholders in our template t = t.replace(keyRe(key), d[key]); } // Return 'rendered' template return t; }; return template; });
The new function keyRe is not exposed through a property on the template object, so it's therefore not accessible to other modules. You can try calling keyRe() from the boot module, or even try template.keyRe, but both attempts would fail. (zip file of current progress)
This is all fine and dandy, but if we wanted to edit HTML templates, it would be cumbersome to use JavaScript strings, right? So, we obviously need a way to load templates from text files.
Before we get to that, let's first define a template that we'll use. Create a directory called templates in your js directory:
mkdir js/templates touch js/templates/main.tpl
Now edit main.tpl to look like this:
<h1>RIA test</h1> <p>Hi, it's now $time, and you're looking at foxbunny's <em>AMD tutorial</em>.</p>
That's done. But how do we load it? RequireJS sports a plugin system, which allows for various load plugins to be used. One such plugin is the aptly named 'text' plugin, which can be used to load plain-text files from a server. Download that plugin and put it in js directory:
cd js wget http://requirejs.org/docs/release/1.0.7/comments/text.js cd ..
Let's modify our boot module to use this plugin and fetch main.tpl:
require( [ 'jquery', 'lib/template', 'text!templates/main.tpl' ], function( $, template, tMain ) { $(document).ready(function() { $('body').append(template.render(tMain, {time: new Date()})); }); });
As you can see, I've changed the layout a bit so I can keep better track of dependencies. The way we used the text plugin is we just prepended text! to the path of the template file we wanted to load. Note that we must also include the extension in the template's path. (zip file of current progress)
If you are using Chrome, the text plugin may trigger a security error "NETWORK_ERR: XMLHttpRequest Exception 101" because Chrome does not allow AJAX requests to be made to local file system. If you want to get around this, you must use a server to server your project.
Let's add some stylesheets to spice things up a bit. We'll divide the CSS properties into two modules, one dealing with layout, and one dealing with typography. In reality, you might take a completely different approach, and divide stylesheets by functionality, or by widget. The basic principle of writing modular stylesheets is the same.
Add two files, one called layout.css, and one called type.css to your css directory:
touch css/layout.css touch css/type.css
Now edit layout.css to look like this:
* { margin: 0; padding: 0; } body { margin: 30px; width: 300px; border-radius: 10px; background: #996; padding: 1em; box-shadow: 0 0 10px rgba(0, 0, 0, 0.7); border: 1px solid #fff; } html { background: #588; }
Contents of the type.css might look like this:
html, body { font-family: Helvetica, Arial, "Liberation Sans", sans-serif; font-size: 14px; } h1 { font-size: 2em; margin-bottom: 1em; color: #ee9; }
To load these modules, just use the @import rules (note that if you add any CSS properties to main.css, the @import rules must be at the top of the file, or they won't work). Edit the main.css file to look like this:
@import url('./layout.css'); @import url('./type.css');
The rendered page now looks more acceptable, depending on where you come from. (your zipball with stylesheets)
Now that you've learned how to use RequireJS (I hope you have), and also a bit about organizing your code, let's take a look at our payload.
That's 10 requests and 348kb of payload right there. A bit on the heavy side. But no matter how heavy, the number of requests is always the main reason your site loads so slow (or maybe it doesn't, but if it did, it would be the number of items, and then the size of the items).
We can cut this down to 4 (one for index.html, one for require.js loader, one of all other JavaScript files, and one for all CSS). Technically, you can even remove require.js from the equation by including it with other JS files, but I'll leave that as an exercise to you.
The reason we installed NodeJS before is because of this. RequireJS project provides a nice optimization tool called r.js which will help us package our project into a set of 4 files we mentioned above.
First thing first, let's install r.js:
Note the -g flag which tells NPM (Node package manager) to install the requirejs package globally. With that done, we now have access to r.js command. Before you can try this new toy, you need to write a project description. Just grab this one and put it in a file called app.build in the test directory:
({ appDir: ".", baseUrl: "js", dir: "../build", uglify: { top_level: false, accii_only: false, beautify: false }, optimizeCss: "standard", inlineText: true, findNestedDependencies: true, modules: [ { name: "boot", path: "js/boot" } ] })
You can read more about what each option does by looking at the 'full version' of the build file in r.js's github repository. In our case, we optimize CSS, inline any plain-text/HTML templates into modules that require them (so that they won't be loaded separately after the page is loaded), and specify a few options for uglifyJS (JavaScript minifier) to avoid headaches with non-ASCII characters, etc.
The most important options above are appDir which points to a directory relative to the build file, and dir, which specifies where the optimized project will be output. Note that the output directory will contain everything that your source directory does.
Now that we've got our build profile, we can proceed to actually build it. (zip file of the current progress)
To build it simply run this command:
The built project will be dumped into a directory called build just outside the test directory.
Loading the built project now gives us this in our network tab:
Yes, that's right. 4 requests, and exactly 114kb of payload, all your dependencies and templates neatly packed into a single JS file, and all your CSS modules neatly packed into a single CSS file. How cool is that!
You've hopefully learned how to write AMD modules, and use them with RequireJS. You've possibly learned a thing or two about writing modular code in general. And you've learned a thing or two about how to spare your users from that 5-minute load time. But if you're not used to rich internet applications in general, you might be missing a point here. So, load up that test page again, right-click on it, and do "View source". Here's what it looks like here:
<!doctype html> <html> <head> <title>RIA test</title> <link rel="stylesheet" href="css/main.css"> <script src="js/require.js" data-main="js/boot"></script> </head> <body> <!-- move along, nothing to see here! --> </body> </html>
Yes, <body> is completely empty. Even though the page does contain things, and the developer tools show you all the elements on the page, the "View source" function is empty because it is showing you the page as it was originally loaded from the server. We've actually built the whole page using JavaScript.
Yeah, I knew you knew. These things aren't exactly news. I hope, though, that RequireJS has made your work much easier, and much more organized.
The source code of this project can be downloaded from its GitHub repository. Feel free to use it as you see fit, without any restrictions. You can also use it as a template for your future projects. I use this template in production at Monwara LLC, so it should work.