Using RequireJS For Asynchronous Script Loading And JavaScript Dependency Management
Traditionally, when I've needed to load multiple JavaScript files into an application, I've used multiple Script tags. This works, but is less than optimal when it comes to load times and organization. A while back, I experimented with LABjs for more efficient asynchronous script loading. LABjs has a dead-simple API and allows for good dependency management. As a further experiment, I wanted to try and convert my LABjs code into RequireJS code. RequireJS, like LABjs, allows for asynchronous JavaScript loading and dependency management; but, RequireJS uses a much more modular approach to dependency definitions.
This is just an initial exploration of RequireJS. RequireJS seems to be quite robust and includes optimization and "build" tools for deployment. For this demo, all I want to do is translate my LABjs files into RequireJS files, load them asynchronously, and make sure that all of the dependencies work nicely together.
To refresh your memory, we're going to be working with three main JavaScript classes (not including jQuery and RequireJS):
- Friend - Defines a friend model that can be given a Pet property.
- Pet - Defines a pet model that can be several different animal types.
- CatLover - Defines a specialized friend that will only accept cats as pets.
All of these classes depend on jQuery. CatLover depends on the existence of Friend, for sub-classing, and Pet, for setter validation. We're going to define each of these as a RequireJS module.
To start, we need to define our main HTML page. This page will load the RequireJS library and then define the initial JavaScript file.
<!DOCTYPE html>
<html>
<head>
<title>
Playing With RequireJS For JavaScript Dependency Management
</title>
<!--
Load the RequireJS library; the "data-main" attribute
will tell the RequireJS library which JavaScript file to
load (main.js) after itself has loaded.
-->
<script
type="text/javascript"
src="./require.js"
data-main="./main">
</script>
</head>
<body>
<!-- Left intentionally blank. -->
</body>
</html>
Notice that our Script tag defines a "data-main" attribute. This tells the RequireJS library which JavaScript file to load once the RequireJS library has, itself, been loaded. This "main.js" file will take care of loading the rest of the JavaScript files.
NOTE: I am not 100% clear on how pathing works in RequireJS. There appears to be all kind of pathing and configuration options; but, for this demo, I'm just putting everything in the demo directory until I get a sense for how the code fits together.
Let's take a look at the main.js file to see what RequireJS is doing once it has been loaded.
Main.js (Our JavaScript Application File)
// Log the start of the file.
console.log( "START: main.js" );
// This is our main script. It will run once RequireJS (and this
// file) have been loaded into the page. It has some class
// dependencies for execution.
//
// NOTE: jQuery isn't loaded as a module; it's simply added to the
// global name-space.
require(
[
"jquery-1.6.4.js",
"friend",
"pet",
"cat-lover"
],
function( _jquery_, Friend, Pet, CatLover ) {
// Create a cat-lover.
var sarah = new CatLover( "Sarah" );
// Create a cat.
var mrMittens = new Pet( Pet.CAT, "Mr. Mittens" );
// Associate the kitty with the cat-lover.
sarah.setPet( mrMittens );
// Log a success.
console.log( "Oh " + sarah.getPet().getName() + "! You're so cute!" );
// Log that jquery was loaded into the global name-space.
console.log( "jQuery", $.fn.jquery, "loaded!" );
}
);
// Log the end of the file.
console.log( "END: main.js" );
The main.js file is not a "module" - it's just a plain JavaScript file. To demonstrate this fact, I've put some logging at the start and end of the code to show that it will execute from the top-down. In between the two log statements, however, we are using the require() method. With RequireJS, there appear to be two primary worker methods:
- require( dependencies, callback )
- define( dependencies, callback )
NOTE: These are not the full method signatures - there are optional arguments available.
require() executes code once the given dependencies have been loaded. define() does the same thing - executes code after dependencies have been loaded; but, the return value of the define()-based callback is used to define a module within the application. In our main JavaScript file, all we're saying is that we want to execute a block of code after the given dependencies have been loaded:
- "jquery-1.6.4.js",
- "friend",
- "pet",
- "cat-lover"
Notice that the jQuery dependency ends with a ".js" extension while the other three do not. This is meant to indicate to the RequireJS loader that the jQuery file path is a plain JavaScript file and not a RequireJS module.
NOTE: There is a version of RequireJS that preloads jQuery, which means it doesn't have to be loaded as an external dependency.
Once the given dependencies have been loaded, the modules are passed to the given callback invocation. In our case, that allows us to create an instance of a PetLover, an instance of a Cat, and associate the two together. Running the above code gives us the following console output:
START: main.js
END: main.js
Oh Mr. Mittens! You're so cute!
jQuery 1.6.4 loaded!
As you can see, the START/END logging executed before the require() callback. This makes sense as the requrie() needs to load dependent scripts asynchronously, allowing the rest of the JavaScript file (main.js) to run in the meantime.
So far, so good! Our main application file was able to run code based on the given dependencies. Now, let's take a look at how the three class dependencies were defined as RequireJS modules.
When you define a RequireJS module, you use the define() function. The define() function takes an optional first argument that associates an explicit "path" with the module. If you are using one module per file, this argument doesn't seem to be necessary - the module will be implicitly associated with the file path. If you are defining multiple modules per file, however, you do need to explicitly define the name of the module.
NOTE: The "Build" tools, which I have not looked at, appear to do some of this naming work for you when they combine individual modules for deployment.
Ok, let's take a look at the Friend.js file which defines the "Friend" module.
Friend.js
// Define the Friend class. This depends on the existence of the
// jQuery library for its definition.
//
// NOTE: jQuery isn't loaded as a module; it's simply added to the
// global name-space.
define(
[
"jquery-1.6.4.js"
],
function( _jquery_ ){
// I am an internal counter for ID.
var instanceID = 0;
// I am the class constructor.
function Friend( name ){
// Get the instance ID.
this._id = ++instanceID;
// Store the name value.
this._name = name;
// Store a default value for pet.
this._pet = null;
}
// Define the prototype.
Friend.prototype = {
// I return the id.
getID: function(){
return( this._id );
},
// I return the name.
getName: function(){
return( this._name );
},
// I return the current pet.
getPet: function(){
return( this._pet );
},
// I set the new name.
setName: function( name ){
// Store the new value.
this._name = name;
// Return this object reference for method chaining.
return( this );
},
// I set the new pet.
setPet: function( pet ){
// Store the new value.
this._pet = pet;
// Return this object reference for method chaining.
return( this );
}
};
// -------------------------------------------------- //
// -------------------------------------------------- //
// Return the constructor.
return( Friend );
}
);
As you can see in the code, the entire content of the file is wrapped in a define() function call. This call lists the dependencies needed to run the code; and, the callback is expected to return the module definition. In our case, the module definition is the Friend constructor function.
The Pet class is pretty much the same thing:
Pet.js
// Define the Pet class. This depends on the existence of the
// jQuery library for its definition.
//
// NOTE: jQuery isn't loaded as a module; it's simply added to the
// global name-space.
define(
[
"jquery-1.6.4.js"
],
function( _jquery_ ){
// I am an internal counter for ID.
var instanceID = 0;
// I am the class constructor.
function Pet( type, name ){
// Get the instance ID.
this._id = ++instanceID;
// Store the type.
this._type = type;
// Store the name value.
this._name = name;
}
// Define some constants.
Pet.CAT = 1;
Pet.DOG = 2;
Pet.BIRD = 3;
Pet.FISH = 4;
Pet.RODENT = 5;
// Define the prototype.
Pet.prototype = {
// I return the id.
getID: function(){
return( this._id );
},
// I return the name.
getName: function(){
return( this._name );
},
// I return the current type.
getType: function(){
return( this._type );
},
// I set the new name.
setName: function( name ){
// Store the new value.
this._name = name;
// Return this object reference for method chaining.
return( this );
}
};
// -------------------------------------------------- //
// -------------------------------------------------- //
// Return the constructor.
return( Pet );
}
);
In both the Friend and the Pet module, we are defining jQuery as a dependency. This means that RequireJS will make sure jQuery is loaded before invoking the given callback. Be mindful, however, that since jQuery is not a proper RequireJS module, it is not actually passed to the callback as a parameter (jQuery is defined in the global name-space). Instead, NULL gets passed-through as the first argument.
The CatLover module is where it gets a bit more exciting; since CatLover sub-classes Friend and requires Pet, its define() function call must list both Friend and Pet as a dependency:
Cat-Lover.js (Sub-Classing Of Friend.js)
// Define the Cat Lover class. Since this is a specialized version of
// a Friend (that loves a certain kind of animal), it depends on the
// existence of both the Friend and Pet classes.
//
// NOTE: jQuery isn't loaded as a module; it's simply added to the
// global name-space.
define(
[
"jquery-1.6.4.js",
"friend",
"pet"
],
function( _jquery_, Friend, Pet ){
// I am the class constructor.
function CatLover( name ){
// Call the super constructor.
Friend.call( this, name );
}
// Extend the Friend class.
CatLover.prototype = Object.create( Friend.prototype );
// Override the the setPet() to make sure that the type
// is a Cat - d'uh; I mean come on... cat's are amazing.
// If you're not a cat lover, in some sense, you're just
// fooling yourself.
CatLover.prototype.setPet = function( pet ){
// Make sure the pet is the good kind.
if (pet.getType() !== Pet.CAT){
// WTF?!
throw( new Error( "InsufficientPetAwesomeness" ) );
}
// This Pet is the right kind. Pass off to super class.
return(
Friend.prototype.setPet.call( this, pet )
);
};
// -------------------------------------------------- //
// -------------------------------------------------- //
// Return the constructor.
return( CatLover );
}
);
Before I started this exploration, I thought it was going to be rather overwhelming. So much of what I've seen about RequireJS talks about the "build" process that I was convinced I wouldn't even be able to get this working. But, as I started wiring this together, I realized that I didn't even need to touch the build process. In fact, this whole thing just "worked" out of the box. Overall, putting this together took nothing more than wrapping my existing code in require() and define() function calls. Hella sweet!
Want to use code from this post? Check out the license.
Reader Comments
Well, Cody Lindley just gave you a Twitter plug:
codylindley codylindley
Using RequireJS For Asynchronous Script Loading And JavaScript Dependency Management - bit.ly/tw1nuv
As I told you on Twitter, I attended a DC jQuery Meetup meeting yesterday, in which the speaker characterized require.js support as a major new feature of jQuery 1.7. He said he got this info at the Boston jQuery conference that just happened. He referred to it as AMD support. But there isn't any mention of require.js or AMD on the 1.7 RC 1 announcement at http://blog.jquery.com/
In fact, I can't find any mention of require.js or AMD support in the 1.6.4, 1.6.3, 1.6.2 or 1.6.1 announcements either. I guess that's the benefit of going to the Boston conference. You hear about stuff that wasn't emphasized in the release announcements.
... er, I should have said, "a benefit".
Check out my post on using RequireJs along with loading jquery as a named module and also shows the build script http://integralist.co.uk/post/11705798780/beginners-guide-to-amd-and-requirejs
@WebManWalking,
Yeah, I am not seeing it on the jQuery blog either. I see that Addy Osmani mentioned it here:
http://addyosmani.com/blog/jquery-17-preview/
... but, I have not actually read Addy's blog post yet. I'll have to take a look tonight.
@Ben
thanks for a great post.
just curious if you have you ever used yepnope http://yepnopejs.com/
I heard it is was very easy to use, supports conditional loading, is integrated into Modernizr and is super small and fast..
if you had then how do they stack up?
Great post Ben! On a side note, you should have a look at cfstatic. It handles JavaScript Dependency, Combining of JS & CSS into single files, Minification, and .less compiling. Not to mention that it is an open source CF project.
https://github.com/DominicWatson/cfstatic
Check out the wiki on github for more info.
Ben,
Which one would you prefer then - LabJS or RequireJS, and why?
@Ben,
So, if I'm reading the Addy Osmani article correctly, although you CAN load lower versions than 1.7 using require.js, you might accidentally load multiple copies. And that won't happen with 1.7's AMD support.
Wish I knew more about AMD's library level support. It's touched upon in the article, but not made explicit: "... it helps avoid issues with other third party code on a page accidentally loading up a version of jQuery on the page that the page owner wasn't expecting."
2 months ago, I gave a talk at the same DC jQuery Meetup on using just Sizzle if you didn't need the overhead of jQuery (file size, load-time processing) or jQuery collections (Sizzle returns arrays = faster than collections). AMD version control would fit in great with that, I would hope:
Suppose I don't really need all of jQuery, just the selection-by-CSS-selector capability. I'd like to use Sizzle if jQuery isn't loaded, but jQuery instead if it is loaded. So all I really *REQUIRE* is Sizzle, so that's what I would specify in my require call. I'm hoping that require can detect that jQuery includes Sizzle and refrain from loading Sizzle if jQuery is also being loaded by some other library. (Minified Sizzle is 1/6th the size of minified jQuery, a really big savings. But you don't want to load Sizzle in ADDITION to jQuery, right?)
In the completion routine of the require call, I could define my own, namespaced, selection-by-CSS-selector function to be either Sizzle() or jQuery.fn.find(), depending on what got loaded. That's the sort of thing I'd like to do if AMD's version control can handle it.
One can dream. :-)
@Ben,
Just found another Addy article that looks at AMD in depth, with examples:
http://addyosmani.com/writing-modular-js/
It prompted me to look at jQuery 1.7rc1's call to define(). (It was tedious to find because of all the references to "undefined" you have to skip, as you might imagine.) Anyway, jQuery 1.7 gives itself the AMD module id "jquery". Plenty of self-documentation precedes the call to define().
Still not too sure how I would go about implementing my load-Sizzle-only-if-jQuery-not-loaded idea. I guess, at a minimum, I would have to modify Sizzle.js to call define() too. On the other hand, I'll bet the Sizzle team does just that before TOO long.
have you try jqury matrix, it is a smart loader/unloader, check it at github
@Scott,
I've heard about it. They've talked about it on the YayQuery pod case a few times since Alex Sexton is one of the authors (and co-host of the podcast). I sounds pretty cool, and from what people say, is very easy to use. But that's as much as I know about it. I guess the difference between something like that and RequireJS is that RequireJS doesn't do any feature detection that I know of; so, YepNope will allow you to load external JavaScript files, but only if they are required for the given browser.
So, YepNope is more about cross-browser inconsistencies, I think, and RequireJS is more about loading application modules... I think?
@Greg,
Oh cool, I've heard of that in passing but haven't taken a look at it just yet. Thanks for the link - I like anything that integrates with ColdFusion :)
@Sami,
Right now, my limited experience and gut feeling is telling me that I like RequireJS because it keeps all of the modules nicely defined and accessible from with the define() and require() methods. If I were to use LABjs, I think I would have to create my own naming / organization hierarchy to keep things from colliding.
But really, I can't say one way or another.
@WebManWalking,
I wonder if the YepNope stuff that Scott mentioned could be used to load Sizzle *only* if one of the jQuery libraries has not yet been loaded.
@Fred,
I have not heard of that one. I'll take a look, thanks!
Not being anywhere near expert on yepnope I understand it as a shortcut to conditional loading.
instead of doing
if(...) {
require(...);
}else{
require(...);
}
you can write:
var hasAwsomeFeature = true;
yepnope({
test:hasAwesomeFeature,
yep: ['load.js','me.js'],
nope: ['addAwesomeFeature.js'],
both: ['useAwesomeFeature.js']
});
yepnope has other functionality too, but to my understanding it pretty much wraps LABJS adding the test.
As you said Ben, with RequireJS you can do configuration to organize your code like adding paths and much more.
I believe i read or heard Alex Sexton say that he used yepnope on the single page webapps and RequireJS on the more complex ones - but I can'r remember where.
@Ben,
A few hours ago, jQuery 1.7 was officially released, and with it, the first official mention of AMD support:
http://blog.jquery.com/#post-1642
Also in the release announcement, we get to see the rationale for refactoring event binding to the new foundation methods on() and off(). It's not simply for a consistent argument list and "Don't Repeat Yourself". It improved the performance of delegated event handling. Improved it quite a lot in the case of that bad old, nasty browser.
Only 2.56% bigger (minified), but creeping up on 100,000 bytes nevertheless: 94,020. When I started with it, version 1.2.6, not so long ago, the unminified version was 100,196.
@Morten,
Ah, ok, that makes sense. I wonder if you can use YepNope to build up the "dependencies" array for the require() statements. Or maybe that is just missing the point of the ease of the YepNope stuff. I'll have to do some digging into it.
@WebManWalking,
I still don't quite understand what they are talking about when it comes to AMD in jQuery. The only documentation they have seems to point to some Bug tracker that doesn't really help my wrap my head around it :( Am I just being dense about this stuff?
On the other note, I love how they are always findings ways to make this stuff faster and faster!
Silly question:
in your main.js, you are "requiring" the existence of:
friend
pet
cat-lover
And then you go on to define:
Friend.js which creates a Friend class
Pet.js which creates a Pet class
and
Cat-Lover.js which creates a CatLover class.
How are the requirements specifically mapped in your main.js to the classes? Is it filenames? Classnames? both? If so, what is the convention?
If I have a js file which is":
FooBar.js:
which then contains:
What would I require? (Probably it would require re-factoring, I'm just wondering how the convention maps.