Dojo provides a feature-rich system for including JavaScript modules. Before we begin this journey to explore this concept in depth, you should know that absolutely no knowledge of the Dojo module, packaging, and build system are required to use Dojo.
You can easily get started using Dojo by using a script element referring to a copy of Dojo on the AOL or Google CDNs. If you want to host your own version of Dojo, you can easily download dojo.js
, include it in a web page using a script element, and be off and running with Dojo Base.
For those new to Dojo, the following resources give a quick overview of Dojo Base:
In general, dojo.js is a lot like jquery.js or prototype js: you get a competitive set of features founds in most JavaScript libraries that are essential for building great web applications. Those features include:
- JavaScript Language Helpers
- Object utilities
- Array utilities
- DOM Manipulation
- A normalized event system
- Ajax & Cross domain requests
- JSON utilities
- Simple effects
- Browser sniffing
However, Dojo is much more than dojo.js, including tools that are not common among most JavaScript libraries. One of these tools is a module system (aka dojo.require()
). JavaScript and web browsers do not offer the module loading conveniences found in other programming environments, and Dojo helps solve this problem.
Module System
In a nutshell, the module system leverages dojo.require('my.module')
to include JavaScript files into a web page. Conceptually, this is really no different than placing a script element in an HTML page that includes an external JavaScript file.
Let’s assume that you are running a local development environment on your computer at http://localhost:8888
. At the root of this web directory, let’s say you have a copy of the Dojo directory (download Dojo 1.4.3 if you’d like to follow along) and an HTML page, index.html.
index.html
is a simple HTML page that includes dojo.js
.
<html lang=”en”>
<head>
<meta charset=”utf-8″ />
<title>Dojo</title>
</head>
<body>
<script src=”dojo/dojo.js”></script>
</body>
</html>
We now have Dojo Base included in the HTML page and have access to all the functionality that it provides us. Life is good, until we require additional functionality. As an example, I would like to use some of the functionality that is contained in Dojo Core, anything inside of the dojo directory that is not directly provided when including dojo.js
.
Suppose we are building an application that pulls data from the Flickr API. If we use this API, we will need to go cross-domain to get data from the API. Since the ability to do a cross-domain request for JSON data is not built into Dojo Base, we need to include code from Dojo Core. So, let’s get back to our index.html
page. To load cross-domain functionality, we could simply use a script element and include the needed Dojo file:
<html lang=”en”>
<head>
<meta charset=”utf-8″ />
<title>Dojo</title>
</head>
<body>
<script src=”/dojo/dojo.js”></script>
<script src=”/dojo/io/script.js”></script>
</body>
</html>
Using a script element gets the job done, but it does not leverage the module system included with Dojo Base. From the viewpoint of Dojo’ers the /dojo/io/script.js
file is a module, and as such can be loaded into a web page using dojo.require()
. Below we load dojo.io.script
using dojo.require()
.
<html lang=”en”>
<head>
<meta charset=”utf-8″ />
<title>Dojo</title>
</head>
<body>
<script src=”/dojo/dojo.js”></script>
<script>
dojo.require(“dojo.io.script”);
//Note: do not include the .js
</script>
</body>
</html>
But what have we gained? Two ways of doing the same exact thing? Well, using the dojo.require()
method actually provides us with some functionality that using a simple script element does not. By using the dojo.require()
method we gain a module system, which provides a few very important features that can help facilitate the building of complex web applications. We are going to examine these features throughout the rest of this article:
- Cache management
- Namespace Management (isolated example of namespace code)
- Path Management
- Dependency Management
Let’s start with cache management, as this feature is relatively simple and does not require significant explanation. By using dojo.require()
, Dojo will prevent the same script from loading twice. If a script is cached in the browser, it uses the cached resource and thus optimizes our code by removing unnecessary HTTP requests. Essentially you can use as many dojo.require()
‘s as you’d like to include the same modules, and Dojo is intelligent enough to only request it once!
The next feature we will investigate requires us to create our own modules. Before we begin, let me remind you that when dealing with browsers and JavaScript, namespaces are critical so as not to risk polluting the global window scope. If you are curious about modules and namespaces, just review the Dojo source code. All of Dojo is organized into modules/namespaces. And, remember, we have already used dojo.require()
to include the predefined dojo.io.script.js
module.
Let’s continue with our Flickr API example. Since we intend to build a very large web application that uses Flickr photo data, we want to organize the code so that it is easy to manage. Essentially, we want to create a flickrApp namespace to store all of the application programming logic. To do this, we update the directory structure to include the file flickrApp.js
.
If you look at JavaScript development in the Dojo way, the flickrApp.js file is actually a module. However, in order for Dojo to truly consider it a module, we have to tell Dojo. To do that, we use the <a id="jhg." title="dojo.provide()" href="http://www.dojotoolkit.org/reference-guide/dojo/provide.html">dojo.provide()</a>
method to initialize the file as a Dojo module. We do that by adding the following to the flickrApp.js file:
Basically, dojo.provide()
creates an object structure (namespace) based on the string that you pass it. In our situation, it creates an object called flickrApp. With that object created, we can now define aspects of the Flickr application as properties of this object. Here’s an example of what the flickrApp.js file could look like:
//start creating the application logic for my flickr app
flickrApp.getData = function(){};
Now that we have our module namespace defined we can leverage dojo.require()
and include our custom module in an HTML page:
<html lang=”en”>
<head>
<meta charset=”utf-8″ />
<title>Dojo</title>
</head>
<body>
<script src=”/dojo/dojo.js”></script>
<script>
dojo.require(“dojo.io.script”);
dojo.require(“flickrApp”);
</script>
</body>
</html>
Do not be fooled, there is some magic here. How is it possible for Dojo to know where in the file system (or web directory) the flickrApp.js
module is located? This is where path management comes into to play. Dojo assumes by default that the string you pass dojo.provide()
mimics the directory structure located one directory up from the dojo.js
file. What is one directory up from the dojo.js
file? The directory that contains the dojo folder.
In other words, dojo.js
is located at dojo/dojo.js
, Dojo will look for modules at , one directory up from
dojo.js
. To reinforce this concept, let’s change the file structure so that it is more organized. Let’s now consider the changes required if our directory structure looked like this:
With this change all of the application code is inside a directory (namespace) called flickrApp. Within this directory, we can further separate the application logic into modules. The first module we will need is the data.js
module containing the logic for getting Flickr photo data, cross-domain, and returning that data to the application. With this change, we need to place inside of data.js
a dojo.provide()
statement passing it the new directory structure. The contents of the data.js
file could look like this:
dojo.provide(“flickrApp.data“);
// Note: do not include the .js
flickrApp.data.getData = function(){};
Remember, Dojo assumes by default that the string passed to dojo.provide()
mimics the directory structure one directory up from the dojo.js
file. In other words, dojo.js
is located at dojo/dojo.js
, so Dojo will look for our new data.js
module at flickrApp/data.js
.
Now, our HTML markup will be changed to reflect the new organization of the code.
<html lang=”en”>
<head>
<meta charset=”utf-8″ />
<title>Dojo</title>
</head>
<body>
<script src=”/dojo/dojo.js”></script>
<script>
dojo.require(“dojo.io.script”);
dojo.require(“flickrApp.data“);
</script>
</body>
</html>
OK, a quick sanity check: is all this really necessary? Why not just include a single JavaScript file that contains all of the application logic and forget this module system? You could, and the application could run with or without the module system. However, Dojo uses the module system because separating web applications into organized modules makes building and maintaining applications easier, and it also helps to optimize the code using the build tools. This is essentially the difference between storing paperwork in a stack or in a file cabinet. The file cabinet requires more work to initially setup and to keep organized, but it saves time as the number of papers increases. Unless, of course, you only need to manage a handful of papers.
I hope you are starting to see the purpose of the module system. But we’re not done yet. Next up is the most important part: dependency management.
Modules can contain references to other modules. Or, stated another way, you can require()
modules that can require()
other modules. And Dojo can help you manage these dependencies! Let’s examine our HTML markup again.
<html lang=”en”>
<head>
<meta charset=”utf-8″ />
<title>Dojo</title>
</head>
<body>
<script src=”/dojo/dojo.js”></script>
<script>
dojo.require(“dojo.io.script”);
dojo.require(“flickrApp.data”);
</script>
</body>
</html>
Remember we are including the dojo.io.script.js
module from Dojo Core. We did this because the Flickr application will need this module. Leveraging the module system and dependency management we can actually remove this require()
statement and place it inside of the data.js
module. Essentially we are saying that the data.js
module is dependent upon the dojo.io.script.js
module. Dojo will manage this dependency, so data.js
could look like this:
dojo.require(“dojo.io.script”);
// Note: dojo.require() should be used after dojo.provide()
flickrApp.data.getData = function(){};
The HTML will now contain only a single require()
statement to include data.js
module.
<html lang=”en”>
<head>
<meta charset=”utf-8″ />
<title>Dojo</title>
</head>
<body>
<script src=”/dojo/dojo.js”></script>
<script>
dojo.require(“flickrApp.data”);
</script>
</body>
</html>
Dojo will now handle the dependency and make sure data.js
is in fact dependent on dojo.io.script.js
.
But wait, there is more. The dependency management provided by the module system needs to be notified when all dojo.require()
statements (and their recursive dependencies) have loaded. This is done by leveraging the dojo.ready()
method which will register a function to be invoked once the DOM is ready and all modules and their dependencies have been loaded and parsed by the JavaScript engine. So, when using the module system it is a best practice to use the dojo.ready()
before making use of any of the functionality provided by predefined or custom modules.
<html lang=”en”>
<head>
<meta charset=”utf-8″ />
<title>Dojo</title>
</head>
<body>
<script src=”/dojo/dojo.js”></script>
<script>
dojo.require(“flickrApp.data”);
dojo.ready(function(){
// Note that dojo.ready() is a shortcut for dojo.addOnLoad() added in Dojo 1.4
// Run code from data.js and all its dependencies safely
});
</script>
</body>
</html>
Before we move on, it’s worth noting that dojo.ready()
can be used anytime, even from within the callback function of a dojo.ready()
. This means that you can embed a dojo.ready()
inside of an dojo.ready()
. This allows us to have a tree structure of dependencies and loading phases. For example:
dojo.ready(function(){
//run code from some.module.js and all its dependencies safely
dojo.require(‘some.other.module’);
dojo.ready(function(){
//run code from some.other.module.js and all its dependencies safely
});
});
We have come a long way. Now, let’s go a bit futher. As previously mentioned, the module system does some path management for us. And, by default, this path is set to the parent directory of dojo.js
. This means that when you require()
anything, Dojo by default will look for it in the parent directory of (relative to) dojo.js
. As an example, if we require('some.other.module')
by default it will look for some/other/module.js
starting one directory up from dojo.js
:
We can now begin to look at how to override the default path used by Dojo to include custom modules, allowing us to customize the location Dojo will look for modules. Let’s update our directory to something more typical of a Dojo application:
Based on this new directory structure, we have taken our some.other.module.js
out of the Dojo parent directory where Dojo will search for modules. Our some.other.module.js
is actually another directory up, inside of the js
directory. Because of this we have to notify Dojo of this change. To understand how this is done, let’s open up the index.html. Before we dojo.require()
the some.other.module.js
file, let’s tell Dojo the location of the some
directory. This is done by using dojo.registerModulePath()
:
<html lang=”en”>
<head>
<meta charset=”utf-8″ />
<title>Dojo</title>
</head>
<body>
<script src=”/dojo/dojo1.4.1/dojo.js”></script>
<script>
dojo.registerModulePath(“some”, “../../some/”);
dojo.require(“some.other.module”);
</script>
</body>
</html>
Based on our directory structure, the dojo.registerModulePath("some", "../../some/")
statement is necessary so that Dojo knows where to find the custom module. We are telling Dojo that the some
namespace/module can be found two directories up (../../
) from dojo.js. Now that Dojo knows where to look, it can resolve the entire namespace some.other.module
and include the some.other.module.js
file. From this point on, Dojo will find any module contained within the some
namespace two directories up from dojo.js
. This is all required because, for security reasons, JavaScript applications in the browser do not have access to the file system structure on your web server.
If you are familiar with using djConfig then this configuration object can also be used to register module paths. All paths set using djConfig
automatically call dojo.registerModulePath()
:
<html lang=”en”>
<head>
<meta charset=”utf-8″ />
<title>Dojo</title>
</head>
<body>
<script type=”text/javascript”>
var djConfig = {modulePaths:{“some”:”../../some/”}};
</script>
<script src=”/dojo/dojo1.4.1/dojo.js”></script>
<script>
dojo.require(“some.other.module”);
</script>
</body>
</html>
You might think I have covered everything related to the module system, however, there are many other facets. One thing I will mention is that everything we have done in this article assumes you are working with a local version of Dojo. Since Dojo can be included cross-domain from the AOL or Google CDN, the module system has to adjust to support this flexibility. Behind the scenes, the module system is based on making XHR requests for modules. This changes if you are using a CDN version of Dojo, as the module system will then switch over to cross-domain mode and include modules using on-demand script elements. This is a topic for another article, but if you are including Dojo from a CDN, you’ll have to register all modules paths and set the baseURL in Dojo.
Beyond Dojo
Finally, these ideas from Dojo can also be used without Dojo at all. YUI 3.0 has taken a similar approach, and there are stand-alone systems that can be used with any library or toolkit. One such system is RequireJS, which is based on the Dojo module system. If you’d like to isolate the concept of a module loading system in order to understand its capabilities, check out RequireJS for more information.