Goin' Full Require: Turtles All The Way Down
ow that we are comfortable the syntax for creating and loading modules we need to set up configure **RequireJS** to load anything and everything we want for our applications. Luckily the configuration API for RequireJS is rather straight forward and allows for a lot of flexibility. Configuring RequireJS is a simple as passing it an Object literal.
var SiteName = require({ });
Pretty simple! The require function returns a configured instance of RequireJS so you can stash it in a variable and use it as the loader for a specific set of modules, or as it if were your applications primary name space. Now, there are 3 options in particular we are going to want to pay special attention to - basePath, paths, and packages.
BasePath
The basePath setting is the default location where RequireJS tries to load modules from by resolving a URL for a script tag.
var SiteName = require({
basePath:"/js/modules"
});
This tells Require that, unless told otherwise, to load any module name from the /js/modules
directory. So if you were to do something like this:
SiteName(["blog", "auth/permissions"], function( blog, permissions ){
var post = new blog.Post();
if( permissions.check( "foobar" ){
// do something
});
});
With the base path configured as previously stated, this call to require is going to try to load two ( 2
) javascript files /js/modules/blog.js
and /js/modules/auth/permissions.js
. This is good for a set it and forget it type of set up. Everything you put in the /js/modules
directory is automatically available without any additional set up.
This is perfect in an ideal world, and you had the foresight to plan a logical folder structure. This isn't always the case and there may even be a situation where you might want to reuse a module that you, or someone else has hosted on a different site. Not to worry, RequireJS has you covered.
Paths
The paths setting take an object literal where the key is the final name of the module and the value is the fully qualified path to the js file ( sans .js ) to load.
var SiteName = require({
basePath:"/js/modules"
, paths:{
moduleA:"../appjs/legacyjs"
, underscore:"http://fake.github.url/master/raw/underscore"
, data:"http://some.service.url/jsonp/data?callback=define"
}
});
All paths are assumed to be relative to the base path, so in the above situation requiring moduleA would resolve to /js/appjs/legacyjs.js
. This is the case unless the path starts with a leading slash ( "/" ), which would be treated as an absolute path, or if it looks like a protocol prefix ( http:
or ftp:
) - in which case the string will be taken as is.
JSONP
Interestingly enough, you can specify a url to a JSONP service which can be used as an AMD module. The service needs to return a single object literal which will be passed directly into the define function. All you need to do is make sure you specify define as the callback parameter for the service. The above configuration would allow you to load code on demand from three ( 3
) different domains across the web and multiple folders on your own site with a single require call.
SiteName(
["blog", "auth/permissions", "moduleA", "underscore", data]
, function( blog, perms, moduleA, _, data){
if( perms.check("foobar")){
_.each(data.news,function( item ){
new blog.Post( item );
});
}
}
);
Packages
The AMD specification also defines the notion on packages. This is standard folder structure for bundling and distributing a collection of AMD modules that only depend on themselves ( think Node.js packages ). And, of course, RequrireJS supports defining and loading AMD packages. This is where the packages setting comes into play. Similar to the way paths works, you'll need to tell Require where to load the package from. A simple package structure might look like this:
MyPackage/
├── moduleA.js
├── moduleB.js
├── moduleC.js
├── moduleD.js
├── ModuleE.js
└── main.js
we can add this package to our set up very easily
var SiteName = require({
basePath:"/js/modules"
, paths:{
moduleA:"../appjs/legacyjs"
, underscore:"http://fake.github.url/master/raw/underscore"
, data:"http://some.service.url/jsonp/data?callback=define"
}
,packages:["MyPackage"]
});
This is the simplest way to include a package, in which require will try to find the main.js
file in a folder called MyPackage
relative to the basePath ( /js/modules/MyPackage/main.js
). Similar to paths, you can specify custom names and locations for packages giving you the ability to load packages from multiple locations or domains
var SiteName = require({
basePath:"/js/modules"
, paths:{
moduleA:"../appjs/legacyjs"
, underscore:"http://fake.github.url/master/raw/underscore"
, data:"http://some.service.url/jsonp/data?callback=define"
}
,packages:[{
name:"mypackage"
,location:"../packages/MyPackage"
}
, {
name:"external"
,location:"http://some.other.domain/packages/External"
}]
});
A setup like this has an immense amount of flexibility as to where your code lives, how and when you load it for your applications. It allows you to break up your code into smaller more logical pieces and only load the bits you really need.
The Module Module
One of the unspoken best practices in the AMD world, is that you don't mess with the global namespace. You should put things there and you should expect anything to be there. If you really need to get at something, you should define it as a dependent module and require it. However, there are situations where you really need to get at something in the global space. For example, this might be something that was executed in a dom ready handler or something was was added to JavaScript in a template that was generated by a server. How do you get access to things kinds of the things? The module module of course.
We didn't talk about this in part one, however, similar to exports, require passes a module commonly called module to your define callback handlers as the third parameter after exports. This module contains information about the current instance of require. One very interesting thing that it allows you to do is associate data to specific modules by name and pull it for later use. It starts with a configuration for RequireJS called config.
(function(){
var template_var = "{{ object.magic }}";
var SiteName = require({
basePath:"/js/modules"
, config:{
moduleA:{
foo:"bar"
,getMagic: function( ){
return template_var;
}
}
}
, paths:{
moduleA:"../appjs/legacyjs"
, underscore:"http://fake.github.url/master/raw/underscore"
, data:"http://some.service.url/jsonp/data?callback=define"
}
,packages:[{
name:"mypackage"
,location:"../packages/MyPackage"
}
, {
name:"external"
,location:"http://some.other.domain/packages/External"
}]
});
}());
What this does is allow us to pull the associated data inside the moduleA moduleA. You do that by calling the config method on the module module. Your moduleA might look a little like this
// /js/modules/moduleA.js
define(function( require, exports, module {
exports.doMagic = function( x ){
var getMagic
,tmp;
// use the module config
tmp = module.config();
return tmp.getMagic() + x;
};
}));
And there you have it. AMD is an effort to give JavaScript a standard style and basis for structured applications. RequireJS has implemented the spec very well and goes a long way to managing large and complex code basis a great deal easier.