In the last few posts we showed how you can create objects in JavaScript. You can define public and private functions, methods, properties in your objects.
But what about dependencies? As we write more and more complex applications, our objects rely on other objects. There becomes a hierarchy. And if that hierarchy is not respected, if one object is run before the object that it requires is loaded, there’s problems.
Developers want to write discrete JS files and modules. We need some sort of #include/import/require. we need the ability to load nested dependencies.
And we want to be able to load our objects asynchronously.
Start with Your Module
Using JavaScript functions for encapsulation you use as the module pattern or it can be the revealing module pattern that you learned about in our previous post.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function () { | |
this.myGlobal = function () {}; | |
}()); |
That type of module relies on attaching properties to the global object to export the module value. The module pattern does not have a way to easily declare its dependencies. The dependencies are assumed to be immediately available when this function executes.
So if for example you are writing a viewmodel that requires Knockout and jQuery, your module just has to assume that both those libraries have already been loaded. And when (not if) it isn’t there… boom.
One fix proposed fix for this is the CommonJS.
CommonJS-style Synchronous Modules
Currently, CommonJS-style synchronous modules are very popular. For example, they are used in Node.js. Such a module looks as follows.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import new modules anywhere | |
var otherModule = require("libs/otherModule"); | |
// Export your functions, classes, objects etc. | |
exports.myFunction = function () { | |
otherModule.otherExportedFunction() + 1; | |
}; |
With this approach, the CommonJS group was able to work out dependency references and how to deal with circular dependencies, and how to get some properties about the current module.
Let’s take that concept a to the next step – asynchronous loading and module definitions.
Asynchronous Module Definition API
The Asynchronous Module Definition (AMD) API specifies a mechanism for defining modules such that the module and its dependencies can be asynchronously loaded. This is particularly well suited for the browser environment where synchronous loading of modules incurs performance, usability, debugging, and cross-domain access problems.
You define a module by including its name before your object. This specification reserves the global variable define so you can define your module.
For example:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import all modules in a single location | |
define([ "libs/otherModule" ], | |
function (otherModule) { | |
// Export your module as an object | |
return { | |
myFunction: function () { | |
otherModule.otherExportedFunction() + 1; | |
} | |
}; | |
} | |
); |
The main advantages are:
- Asynchronous: AMDs have been designed to support asynchronicity and most AMD-compatible script loaders take advantage of it. Thus, AMD modules can be loaded in parallel. Once the last imported module has finished loading, the importing module can be evaluated. It is ironic that the Node.js module system is synchronous.
- Work in browsers: Browser modules have to be asynchronous and AMDs are.
- No globals: AMDs avoid global variables from being created, because there is always a wrapping function. Note, though, that CommonJS-compliant module loaders usually also (invisibly) wrap modules they load in a function.
- No more namespacing: AMDs obviate the need for namespacing such as foo.bar.myModule, via a global variable and nested objects. The namespacing is in the path to the file, a local variable is short and convenient handle.
define()
In AMD, define is a global function that takes the following arguments:
- id. The first argument is a string literal that names your module.
- dependencies. The second argument, dependencies, is an array literal of the module ids that are dependencies required by the module that is being defined.
- factory. The third argument, factory, is a function that should be executed to instantiate the module or an object.
Where Do I Get AMD?
AMD already has good adoption on the web:
- jQuery 1.7
- Dojo 1.7
- EmbedJS
- Ender-associated modules like bonzo, qwery, bean and domready
- Used by Firebug 1.8+
- The simplified CommonJS wrapper can be used in Jetpack/Add-on SDK for Firefox
- Used for parts of sites on the BBC (observed by looking at the source, not an official recommendation of AMD/RequireJS)
Give an AMD loader a try. You have some choices:
If you want to use AMD but still use the load one script at the bottom of the HTML page approach:
- Use the RequireJS optimizer either in command line mode or as an HTTP service with the almond AMD shim.
AMD Goodness
AMD modules require less tooling, there are fewer edge case issues, and better debugging support.
What is important: being able to actually share code with others. AMD is the lowest energy pathway to that goal.
References
Asynchronous Module Definition (AMD) API for JavaScript
The power of the Asynchronous Module Definition
Why AMD? from the Require.JS documentation