Understanding AMD & RequireJS
Common, Apple! Google would never do that. It is a mild form of disease, but still... let’s talk about the cure.
Let’s talk about AMD and it’s usage in the wild. No, not about microchips, but about Asynchronous Module Definition. AMD API specifies a mechanism for defining modules and their dependencies. The beauty is that those modules then can be loaded asynchronously without worrying about the loading order. More about that later.
Why should we care about modules? Do you want your code to be highly decoupled and extremely reusable? You don’t have to answer, it is a rhetorical question. So what is a module? A module can be anything: an object, function, class, or just a code that is executed when a module is loaded.
Whole API consists of a single function. Brilliant! The signature of the function is:
define(id?, dependencies?, factory);
First, two arguments id and dependencies are optional. Third factory is a function that is executed to instantiate a module. If id is not specified then the module name is its file location. If dependencies argument is not specified it means (don’t screw this up!) that the module has no dependencies. This makes code easier to understand and maintain because all dependencies are clearly stated.
Here is a sample module that has dependencies on jQuery and jQuery cookie plugin:
Loaded dependencies are referenced as arguments to your factory function. As you see in the sample above, jQuery is passed as $. And, since the cookie plugin does not actually return any value, we don’t need to reference it in the parameters. We will get back to jQuery plugins later. We are just stating that code inside this module is dependent on the jquery.cookie plugin.
The order of loaded modules (if they are properly defined) does not matter. Factory function is executed only when all dependencies have loaded. Module code is encapsulated and does not interfere with the global namespace.
All modules are asynchronous by nature and are loaded into the browser in a non blocking fashion. You do care about performance, right?
Not convinced yet? Many of the major libraries are using it, including giants such as jQuery, KnockoutJS, Dojo, EmbedJS and many more.
Let’s summarize what advantages over “script hell” it provides:
- Asynchronous by nature
- Dependencies are easy to identify
- Avoids global variables
- Can be lazy loaded if needed
- Easily portable
On top of that:
- Powerful plugin support
- Support script fallbacks (OMG, CDN is down!)
- Can be easily configured
- Can load multiple versions of the same library
Now when we are on the same page of what is AMD, let’s talk about AMD loaders. An AMD loader is code that implements AMD specification. There are multiple implementations but we will focus on RequireJS in this post.
First things first. We need to load RequireJS:
To get things started you need to call require. The first parameter is an array of dependent modules that will be referenced as arguments to your callback function.
In the sample above, if an application module has any dependencies, those dependencies will be loaded as well. This will work well in the simplest possible setup, when all scripts (including RequireJS) are in a single directory. The module name represents the file name. Let’s take a look at RequireJS configuration options, so that you can organize scripts in any way or even load them from CDN.
To configure RequireJS simply call:
If you wish to load jQuery from the CDN, but fallback to a local version in case CDN is not available (yes, that can happen), then provide an array of paths:
All this stuff is good, but all the power of the RequireJS comes when you use an optimizer. What’s an optimizer? It minifies (using UglifyJS or Google Closure Compiler (needs Java)) and combines all modules into a single file. If you load jQuery from CDN, don’t worry it will stay that way.
npm install -g requirejs
Now you are ready. You can either specify options on the command line or in a separate JS file. Our preferred method is a separate build.js file. So that when you call node just pass file path as an options parameter:
node r.js -o build.js
Contents of build.js are following:
baseUrl, is relative to appDir. If no appDir, then baseUrl is relative to the build.js file, or if just using command line arguments, the current working directory. In our case build.js is in the build directory so we are just going one level up.
optimize indicates what minifier should be used. Available options: uglify, uglify2, closure, none.
exclude modules are to be excluded during the optimization process. They will still be loaded when required.
mainConfigFile set this value to your main.js so that you do not have to duplicate configuration for paths and shim.
out path where minified and combined output should be saved.
This will minify and concatenate your source files into a single file. And, as you already saw, you can exclude libraries that you don’t want to load initially to speed up that initial loading. Well done. But wait, we can do more. You probably have a console write stuff in your code, so that when debugging and tracing it works wonders. You don’t want this to go to production. It will increase file size and slow down execution. It may even cause your script to throw an error (cough, IE8). You have an opportunity to sanitize your code from that console stuff. Here is how you do it:
onBuildRead is called for each module and the return value will be passed to minifier.
There is another hook onBuildWrite, which will be called for each module before writing contents to disk. This might be useful if you want to include copyrights or any other additional text.
Let’s see what we have now. Script will load RequireJS, then RequireJS will check data-main attribute and load your entry point. This will result into two http requests. This is not going to fly for performance obsessed people. Let’s shave off that additional request and pack everything into single file:
To shim or not to shim
There are cases when you may need to load or work with libraries that do not use define. For that you can use shim configuration:
It is important in this case to understand dependencies well. Some libraries, if they detect that AMD define is present, will not expose it’s value to a global scope. In this case, you may need to expose it manually before loading the library that it is dependent on in the presence of that global variable.
If you are an author of a library or jQuery plugin, be kind and expose your library as a plugin. Or, optionally specify dependencies so that someone using AMD can use it as a module. This way it can be included as a simple script or referenced as a dependency.
If your library has any dependencies, for example jQuery plugin, you may chose to use the following pattern:
As you can see, we are immediately invoking an anonymous function and as a parameter inlining function which has all the logic inside. This way if define is present, we are not doing anything, just defining a module, otherwise invoking it immediately by passing dependencies (in this case jQuery) from the global scope.
And, to quote RequireJs: “using a modular script loader like RequireJS will improve the speed and quality of your code."