If you've programmed for any amount of time, you've been exposed
to the concept of grouping related pieces of code into related blocks,
whether they be called libraries, packages, or modules, and pulling in
these resources when you need them via a mechanism like an import
statement or a #include
preprocessor directive. Dojo's
official means of accomplishing the same kind of concept is via
dojo.provide
and dojo.require
, respectively.
In Dojo parlance, reusable chunks of code are called
resources and collections of related resources
are grouped into what are known as modules. Base
provides two incredibly simple constructs for importing modules and
resources: dojo.require
and
dojo.provide
. In short, you include
a dojo.provide
statement as the
first line of a file that you want to make available for a dojo.require
statement to pull into a page.
As it turns out, dojo.require
is a
lot more than just a placeholder like a SCRIPT
tag; it takes care of mapping a
module to a particular location on disk, fetching the code, and
caching modules and resources that have previously been dojo.require
d. Given that each dojo.require
statement incurs at least one
round trip call to the server if the resource is not already loaded,
the caching can turn out to be a tremendous optimization; even the
caching that you gain from requiring a resource one time and ensuring
it is available locally from that point forward is a great
optimization.
For anything but the smallest of projects, the benefits of
using this approach are irrefutable. The ease of maintenance and
simplicity gained in extending or embedding code in multiple places
is a key enabler to getting work done quickly and effectively. As
obvious as it may sound, importing code in the manner just described
hasn't always been obvious to web developers, and many web projects
have turned into difficult-to-maintain monstrosities because of
improper source code management in the implementation. For example,
a typical workflow has been to take a JavaScript file that's in a
static directory of a web server and to insert it into the page
using a SCRIPT
tag like
so:
<script src="/static/someScript.js" type="text/javascript"></script>
OK, there's probably nothing wrong with that for one or two
script tags—but what about when you have multiple pages that need
the same tools provided by the scripts? Well, then you might need to
include those SCRIPT
tags in
multiple pages; later on down the road you might end up with a lot
of loose scripts, and when you have to start manually keeping track
of all of them, the situation can get a little bit unwieldy. Sure,
back in the day when a few hundred lines of JavaScript might have
been all that was in a page, you wouldn't have needed a more robust
mechanism for managing resources, but modern applications might
include tens of thousands of lines of JavaScript. How can you
possibly manage it all without a good tool for fetching on demand
and lazy loading?
In addition to mitigating the configuration management
nightmare that might otherwise await you, the dojo.provide
and dojo.require
abstraction also allows the
build tools that are provided in Util to do pretty amazing things
like condense multiple files (each requiring a synchronous request)
into a single file that can be requested and endure much less
latency. Without the right abstractions that explicitly define
dependences, build tool features that could be freebies suddenly
become impossibilities.
A final benefit of a well-defined system like dojo.provide
and dojo.require
is the ability to manage
related resources by clustering them into namespaces so that overall
naming collisions are minimized and code is more easily organized
and maintained. Even though dojo namespaces are
really just hierarchies of nested objects simplified with dot
notation, they are nonetheless quite effective for organizing
namespaces and accomplish the very same purpose.
In fact, organizing resources by namespace is so common that
Dojo provides a Base function called dojo.setObject
. This function works by
accepting two arguments. The first argument is an object hierarchy
that will be automatically created, and the second value is what
will be mapped to the hierarchy:
dojo.setObject(/* String */ object, /* Any */ value, /* Object */ context) //returns Any
Example 2-2 illustrates.
Example 2-2. Namespace organization with dojo.setObject
var foo = {bar : {baz : {qux : 1}}}; //nest some objects the 'long' way console.log(foo.bar.baz.qux); //displays 1 //Or you could opt to do it in one crisp statement without matching all of the braces... dojo.setObject("foo.bar.baz.qux", 1); //crisper syntax console.log(foo.bar.baz.qux); //displays 1 //If you supply an optional context, the Object is set relative to the context instead of //the global context, dojo.global var someContext = {}; dojo.setObject("foo.bar.baz.qux", 23, someContext); console.log(someContext.foo.bar.baz.qux); //displays 23
The use of dojo.setObject
is nothing more than syntactic sugar, but it can significantly
declutter code and the tediousness of matching braces, etc.,
whenever you do need it.
Tip
The OpenAjax Alliance (http://www.openajax.org ), a consortium of vendors banding together to promote openness and standards amongst advanced web technologies, strongly encourages the practice of using dotted object notation to organize namespaces.
A short concrete example is in order to put dojo.require
and dojo.provide
into perspective. First,
consider a simple module that provides a trivial function, such as
Fibonacci. In Example 2-3,
the resource is also associated with a module. Although grouping
resources into modules is not strictly necessary, it is almost
always good practice. Throughout this book, you'll commonly see
dtdg
(for Dojo: The
Definitive Guide ) used to denote a generic namespace for
modules.
Example 2-3. Defining a simple simple module (dtdg.foo)
/* The dojo.provide statement specifies that this .js source file provides a dtdg.foo module. Semantically, the dtdg.foo module also provides a namespace for functions that are included in the module On disk, this file would be named foo.js and be placed inside of a dtdg directory. */ dojo.provide("dtdg.foo"); //Note that the function is relative to the module's namespace dtdg.foo.fibonacci = function(x) { if (x < 0) throw Exception("Illegal argument"); if (x <= 1) return x; return dtdg.foo.fibonacci(x-1) + dtdg.foo.fibonacci(x-2); }
Tip
You will almost always want to group your resources into
logical modules and associate them with a namespace. In addition
to being a good implementation practice, it also prevents you from
inadvertently clobbering symbols in the global namespace as well
as preventing anyone else from doing the same to you. After all,
that's one of the motivators for using dojo.provide
and dojo.require
in the first place!
In another page somewhere, you determine that you want to use
your dtdg.foo
module to amaze the
elementary school math junkies. Instead of rewriting your
well-tested function from scratch and potentially making a mistake
that could lead to embarrassment, you instead decide to reuse it via
dojo.require
. Example 2-4 shows how you would
use a local module in conjunction with the rest of the toolkit being
loaded over the CDN. This example assumes that the following HTML
file is saved alongside a directory called dtdg
that contains the module from Example 2-3.
Example 2-4. Using a local module with XDomain bootstrappping
<html> <head> <title>Fun With Fibonacci!</title> <script type="text/javascript" src="http://o.aolcdn.com/dojo/1.1/dojo/dojo.xd.js" djConfig="baseUrl:'./'"> </script> <script type="text/javascript"> dojo.registerModulePath("dtdg", "./dtdg"); dojo.require("dtdg.foo"); /* at this point, the dojo.require is being satisfied asynchronously because we're using an Xdomain build of Dojo. Better wrap any references to dtdg.foo in an addOnLoad block */ dojo.addOnLoad(function( ) { dojo.body( ).innerHTML = "guess what? fibonacci(5) = " + dtdg.foo.fibonacci(5); }); </script> </head> <body> </body> </html>
The key concept to take away from the previous listing is that
it's fairly straightforward to dojo.require
a resource into the page and
then use it. However, there are a few finer points worth
highlighting.
For local installations, Dojo looks in the root level
directory of the toolkit for modules—but when you're performing
XDomain loading, the "real" root directory of the toolkit would be
somewhere off on AOL's server. Thus, a special configuration switch,
baseUrl
is passed into djConfig
in order to designate the
starting point for looking up local modules—dtdg.foo
in this case.
Tip
djConfig
is simply a
means of providing specific configuration parameters to the
toolkit. It is covered in the next section, but for now, just roll
with it.
The dojo.registerModulePath
function simply associates a top-level namespace, its first
parameter, to a physical directory that is relative to baseUrl
, its second parameter.
Warning
Don't forget to take extra care when configuring module
paths with dojo.registerModulePath
. It is not
uncommon to be off by one level in the directory structure if you
forget that the relative directory is specified to the dojo.js
file—not the root level of the
toolkit. Additionally, ending a module path with a trailing
forward slash has been known to intermittently cause problems, so
you should take care to avoid that practice as well.
Everything that is defined in the custom module is made
available via the call to dojo.require
. For example, if the dtdg.foo
module had contained additional
functions or symbols, they would be available after the dojo.require("dtdg.foo")
statement. As
usual, we didn't reference anything provided by dtdg.foo
outside of the addOnLoad
block.
Tip
There is not necessarily a one-to-one mapping between
.js source files and the functions that are
dojo.provide
d in them, but it
is generally enforced as a matter of style. Exceptions include
cases where some functions might not be exposed because they are
considered private as far as an API is concerned.
You may have also noticed a call to dojo.body( )
in the previous code listing.
Essentially, this call is just a shortcut for returning the body of
the current document—as opposed to document.body
, which is considerably less
convenient.
For comparison purposes, Example 2-5 shows the very same
example, but this time it uses a local installation of Dojo with the
dtdg module located at the root level of the toolkit alongside the
dojo directory that contains Core so that no
reference to baseUrl
or a call to
registerModulePath
is necessary.
This convenience is available because Dojo automatically searches
for modules in the directory alongside Core, which is a logical and
convenient location to maintain them.
Example 2-5. Using dtdg.foo with a local toolkit installation
<html> <head> <title>Fun With Fibonacci!</title> <script type="text/javascript" src="your/relative/path/from/this/page/to/dojo/dojo.js" > </script> <script type="text/javascript"> dojo.require("dtdg.foo"); /* we use an addOnLoad block even though it's all local as a matter of habit*/ dojo.addOnLoad(function( ) { dojo.body( ).innerHTML = "guess what? fibonacci(5) = " + dtdg.foo.fibonacci(5); }); </script> </head> <body> </body> </html>
As an example of some of the concepts from this chapter, let's build a module. Because life can be so hard at times, a magic genie that can give us answers whenever we need it would be a godsend. (Dojo might simply be pure automation that seems like magic, but genies are real magic.)
To get started building a module, recall that it's a good idea to namespace it. Example 2-6 sticks with the dtdg namespace, which we've been using so far in this book, and associates a Genie resource with it. If you don't already have a local directory called dtdg, go ahead and create one now. Inside of it, open up a new file called Genie.js, where we'll include the magic shown in Example 2-6.
Example 2-6. The implementation for a magic genie module
//always include the dojo.provide statement first thing dojo.provide("dtdg.Genie"); //set up a namespace for the genie dtdg.Genie = function( ) {} //wire in some predictions, reminiscent of a magic 8 ball dtdg.Genie.prototype._predictions = [ "As I see it, yes", "Ask again later", "Better not tell you now", "Cannot predict now", "Concentrate and ask again", "Don't count on it", "It is certain", "It is decidedly so", "Most likely", "My reply is no", "My sources say no", "Outlook good", "Outlook not so good", "Reply hazy, try again", "Signs point to yes", "Very doubtful", "Without a doubt", "Yes", "Yes - definitely", "You may rely on it" ]; //wire in an initialization function that constructs the interface dtdg.Genie.prototype.initialize = function( ) { var label = document.createElement("p"); label.innerHTML = "Ask a question. The genie knows the answer..."; var question = document.createElement("input"); question.size = 50; var button = document.createElement("button"); button.innerHTML = "Ask!"; button.onclick = function( ) { alert(dtdg.Genie.prototype._getPrediction( )); question.value = ""; } var container = document.createElement("div"); container.appendChild(label); container.appendChild(question); container.appendChild(button); dojo.body( ).appendChild(container); } //wire in the primary function for interaction dtdg.Genie.prototype._getPrediction = function( ) { //get a number betweeen 0 and 19 and index into predictions var idx = Math.round(Math.random( )*19) return this._predictions[idx]; }
Essentially, the listing does nothing more than provide a
Function object called dtdg.Genie
that exposes one "public" function, initialize
.
Tip
In Dojo, the convention of prefixing internal members that should be treated as private with a leading underscore is common and will be used throughout this book. It's important to really respect such conventions because "private" members may be quite volatile.
The listing is laden with comments, and from a web development standpoint, the logic should hopefully be easy enough to follow. (If it's not, this would be a great time to review some HTML and JavaScript fundamentals elsewhere before reading on.)
To actually put the magic genie to use, we'll need to modify the basic template, as shown in Example 2-7.
Example 2-7. A web page utilizing the magic genie
<html> <head> <title>Fun With the Genie!</title> <script type="text/javascript" src="http://o.aolcdn.com/dojo/1.1/dojo/dojo.xd.js"> djConfig="modulePaths:{dtdg:'./dtdg'},baseUrl:'./'"> </script> <script type="text/javascript"> // require in the module dojo.require("dtdg.Genie"); // safely reference dtdg.Genie inside of addOnLoad dojo.addOnLoad(function( ) { //create an instance var g = new dtdg.Genie; //fire it up, which takes care of the rest g.initialize( ); }); </script> </head> <body> </body> </html>
This example illustrates the reusability and portability of
the dtdg.Genie
module. You simply
require it into the page and, once it's initialized, it "just
works." (And so long as the user doesn't read the source code, it
truly remains magical.) A finer point worth clarifying is the use of
djConfig
to configure Dojo before
bootstrapping: the modulePaths
is
inlined to qualify the location of the module relative to baseUrl
, which is defined as the current
working directory. Thus, from a physical standpoint, the file
structure might look like Figure 2-1.
Get Dojo: The Definitive Guide now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.