In the world of scalable JavaScript, when we say an application is modular, we often mean it’s composed of a set of highly decoupled, distinct pieces of functionality stored in modules. Loose coupling facilitates easier maintainability of apps by removing dependencies where possible. When implemented efficiently, it becomes pretty easy to see how changes to one part of a system may affect another.
In the earlier chapters, we covered the importance of modular programming and the modern way of implementing modular design patterns. While ES2015 introduced native modules to JavaScript, writing modular JavaScript was still possible before 2015.
In this section, we will look at three formats for modular JavaScript using classic JavaScript (ES5) syntax: Asynchronous Module Definition (AMD), CommonJS, and Universal Module Definition (UMD). To learn more about JavaScript modules, please refer to Chapter 5, which covers ES2015+ syntax for module imports, exports, and more.
It isn’t easy to discuss AMD and CommonJS modules without talking about script loaders. Script loading was a means to a goal. Modular JavaScript could be implemented only using compatible script loaders.
Several great loaders were available for handling module loading in the AMD and CommonJS formats, but I personally preferred RequireJS and curl.js.
The AMD format was introduced as a proposal for defining modules in which both the module and dependencies can be asynchronously loaded. The overall goal for the AMD format is to provide a solution for modular JavaScript that developers could use. It has several distinct advantages, including being both asynchronous and highly flexible by nature, which removes the tight coupling one might commonly find between code and module identity. Many developers enjoyed using AMD, and one could consider it a reliable stepping stone toward JavaScript modules, which were unavailable at the time.
AMD began as a draft specification for a module format on the CommonJS list, but as it couldn’t reach full consensus, further development of the format moved to the amdjs group.
It was embraced by projects including Dojo, MooTools, and even jQuery. Although the term CommonJS AMD format has been seen in the wild occasionally, it’s best to refer to it as just AMD or Async Module support because not all participants on the CommonJS list wished to pursue it.
There was a time when the proposal was referred to as Modules Transport/C. However, because the spec wasn’t geared toward transporting existing CommonJS modules but rather for defining modules, it made more sense to opt for the AMD naming convention.
The first two concepts worth noting about AMD are the ideas of a define
method for facilitating module definition and a require
method for handling dependency loading. define
is used to define named or unnamed modules using the following
signature:
define
(
module_id
/*optional*/
,
[
dependencies
]
/*optional*/
,
definition
function
{}
/*function for instantiating the module or object*/
);
As we can tell by the inline comments, the module_id
is an optional argument that is typically required only when non-AMD concatenation tools are being used (there may be some other edge cases where it’s useful, too). When this argument is left out, we refer to the module as anonymous.
When working with anonymous modules, the idea of a module’s identity is DRY (Don’t repeat yourself), making it trivial to avoid duplication of filenames and code. Because the code is more portable, it can be easily moved to other locations (or around the filesystem) without needing to alter the code itself or change its module ID. Consider the module_id
similar to the concept of folder paths.
Developers can run this same code on multiple environments using an AMD optimizer that works with a CommonJS environment such as r.js.
Back to the define
signature, the dependencies
argument represents an array of dependencies required by the module we are defining, and the third argument
(definition
function
or factory function
) is a function that’s executed to instantiate our module. A bare bones module could be defined as in Example 10-1.
define()
// A module_id (myModule) is used here for demonstration purposes only
define
(
"myModule"
,
[
"foo"
,
"bar"
],
// module definition function
// dependencies (foo and bar) are mapped to function parameters
function
(
foo
,
bar
)
{
// return a value that defines the module export
// (i.e., the functionality we want to expose for consumption)
// create your module here
var
myModule
=
{
doStuff
:
function
()
{
console
.
log
(
"Yay! Stuff"
);
}
};
return
myModule
;
});
// An alternative version could be...
define
(
"myModule"
,
[
"math"
,
"graph"
],
function
(
math
,
graph
)
{
// Note that this is a slightly different pattern
// With AMD, it's possible to define modules in a few
// different ways due to its flexibility with
// certain aspects of the syntax
return
{
plot
:
function
(
x
,
y
){
return
graph
.
drawPie
(
math
.
randomGrid
(
x
,
y
)
);
}
};
});
On the other hand, require
is typically used to load code in a top-level JavaScript file or within a module should we wish to fetch dependencies dynamically. An example of its usage is in Example 10-2.
require()
// Consider "foo" and "bar" are two external modules
// In this example, the "exports" from the two modules
// loaded are passed as function arguments to the
// callback (foo and bar) so that they can similarly be accessed
require
([
"foo"
,
"bar"
],
function
(
foo
,
bar
)
{
// rest of your code here
foo
.
doSomething
();
});
Example 10-3 shows a dynamically loaded dependency:
define
(
function
(
require
)
{
var
isReady
=
false
,
foobar
;
// note the inline require within our module definition
require
([
"foo"
,
"bar"
],
function
(
foo
,
bar
)
{
isReady
=
true
;
foobar
=
foo
()
+
bar
();
});
// we can still return a module
return
{
isReady
:
isReady
,
foobar
:
foobar
};
});
Example 10-4 shows defining an AMD-compatible plug-in.
// With AMD, it's possible to load in assets of almost any kind
// including text-files and HTML. This enables us to have template
// dependencies which can be used to skin components either on
// page-load or dynamically.
define
(
[
"./templates"
,
"text!./template.md"
,
"css!./template.css"
],
function
(
templates
,
template
){
console
.
log
(
templates
);
// do something with our templates here
}
});
Although css!
is included for loading Cascading Style Sheets (CSS) dependencies in the preceding example, it’s important to remember that this approach has some caveats, such as not being able to establish when the CSS is fully loaded. Depending on how we approach our build process, it may also result in CSS being included as a dependency in the optimized file, so use CSS as a loaded dependency in such cases with caution. If you’re interested in doing this, we can explore @VIISON’s RequireJS CSS plug-in.
This example could simply be looked at as requirejs(["app/myModule"],
function(){})
, which indicates the loader’s top-level globals are being used. This is how to kick off the top-level loading of modules with different AMD loaders. However, if a define()
function is passed as a local require, all require([]
) examples apply to both types of loader: curl.js and RequireJS (Examples 10-5 and 10-6).
require
([
"app/myModule"
],
function
(
myModule
){
// start the main module which in turn
// loads other modules
var
module
=
new
myModule
();
module
.
doStuff
();
});
curl
([
"app/myModule.js"
],
function
(
myModule
){
// start the main module which in turn
// loads other modules
var
module
=
new
myModule
();
module
.
doStuff
();
});
What follows is the code for modules with deferred dependencies:
<
pre
xmlns
=
"http://www.w3.org/1999/xhtml"
id
=
"I_programlisting11_id234274"
data-type
=
"programlisting"
data-code-language
=
"javascript"
>
// This could be compatible with jQuery's Deferred implementation, // futures.js (slightly different syntax) or any one of a number // of other implementations define(["lib/Deferred"], function( Deferred ){ var defer = new Deferred(); require(["lib/templates/?index.html","lib/data/?stats"], function( template, data ){ defer.resolve( { template: template, data:data } ); } ); return defer.promise(); });</
pre
>
As we’ve seen in previous sections, design patterns can be highly effective in improving how we approach structuring solutions to common development problems. John Hann has given some excellent presentations about AMD module design patterns covering the Singleton, Decorator, Mediator, and others. I highly recommend checking out his slides.
jQuery comes with only one file. However, given the plug-in-based nature of the library, we can demonstrate how straightforward it is to define an AMD module that uses it here:
// Code in app.js. baseURl set to the lib folder
// containing jquery, jquery.color, and lodash files.
define
([
"jquery"
,
"jquery.color"
,
"lodash"
],
function
(
$
,
colorPlugin
,
_
){
// Here we've passed in jQuery, the color plugin, and Lodash
// None of these will be accessible in the global scope, but we
// can easily reference them below.
// Pseudorandomize an array of colors, selecting the first
// item in the shuffled array
var
shuffleColor
=
_
.
first
(
_
.
shuffle
([
"#AAA"
,
"#FFF"
,
"#111"
,
"#F16"
]));
console
.
log
(
shuffleColor
);
// Animate the background color of any elements with the class
// "item" on the page using the shuffled color
$
(
".item"
).
animate
(
{
"backgroundColor"
:
shuffleColor
}
);
// What we return can be used by other modules
return
function
()
{};
});
However, there is something missing from this example, and it’s the registration concept.
One of the key features that landed in jQuery 1.7 was support for registering jQuery as an asynchronous module. A number of compatible script loaders (including RequireJS and curl) are capable of loading modules using an asynchronous module format, which means fewer hacks are required to get things working.
If a developer wants to use AMD and does not want her jQuery version leaking into the global space, she should call noConflict
in their top-level module that uses jQuery. In addition, since multiple versions of jQuery can be on a page, there are special considerations that an AMD loader must account for, so jQuery only registers with AMD loaders that have recognized these concerns, which are indicated by the loader specifying define.amd.jQuery
. RequireJS and curl are two loaders that do so.
The named AMD provides a robust and safe safety blanket for most use cases:
// Account for the existence of more than one global
// instance of jQuery in the document, cater for testing
// .noConflict()
var
jQuery
=
this
.
jQuery
||
"jQuery"
,
$
=
this
.
$
||
"$"
,
originaljQuery
=
jQuery
,
original$
=
$
;
define
([
"jquery"
]
,
function
(
$
)
{
$
(
".items"
).
css
(
"background"
,
"green"
);
return
function
()
{};
});
We have now reviewed several code samples taking us through what AMD is capable of. It appears to be more than just a typical Module pattern, but why was it a better choice for modular application development?
Provides a clear proposal for how to approach defining flexible modules.
Significantly cleaner than the present global namespace and <script>
tag solutions many of us rely on. There’s a clean way to declare standalone modules and dependencies they may have.
Module definitions are encapsulated, helping us to avoid pollution of the global namespace.
Arguably works better than some alternative solutions (e.g., CommonJS, which we’ll be looking at shortly). It doesn’t have issues with cross-domain, local, or debugging and doesn’t rely on server-side tools to be used. Most AMD loaders support loading modules in the browser without a build process.
Provides a “transport” approach for including multiple modules in a single file. Other approaches like CommonJS have yet to agree on a transport format.
It’s possible to lazy-load scripts if this is needed.
Having used AMD for several projects, I conclude that it ticks a lot of the checkboxes that developers creating serious applications might desire from a better module format. It avoids the need to worry about globals, supports named modules, doesn’t require server transformation to function, and is a pleasure to use for dependency management.
It’s also an excellent addition for modular development using Backbone.js, ember.js, or other structural frameworks for keeping applications organized.
As AMD was heavily discussed within the Dojo and CommonJS worlds, we know it’s had time to mature and evolve. We also know it’s been battle-tested in the wild by a number of large companies to build nontrivial applications (IBM, BBC iPlayer), and so, if it didn’t work, chances are they would have abandoned it, but they didn’t.
That said, there are still areas where AMD could have been improved. Developers who have used the format for some time may feel the AMD boilerplate/wrapper code was an annoying overhead. While I share this concern, there were tools such as Volo that helped work around these issues, and I would argue that, on the whole, the pros with using AMD far outweighed the cons.
The CommonJS module proposal specifies a simple API for declaring modules server-side. Unlike AMD, it attempts to cover broader concerns such as I/O, file-system, promises, and more.
Originally called ServerJS in a project started by Kevin Dangoor back in 2009, the format was later formalized by CommonJS, a volunteer working group that aims to design, prototype, and standardize JavaScript APIs. They attempted to ratify standards for both modules and packages.
From a structural perspective, a CommonJS module is a reusable piece of JavaScript that exports specific objects made available to any dependent code. Unlike AMD, there are typically no function wrappers around such modules (so we won’t see define
here, for example).
CommonJS modules contain two primary parts: a free variable named exports
, which includes the objects a module wishes to make available to other modules, and a require
function that modules can use to import the exports of other modules (Examples 10-7, 10-8, and 10-9).
require()
and exports
// package/lib is a dependency we require
var
lib
=
require
(
"package/lib"
);
// behavior for our module
function
foo
()
{
lib
.
log
(
"hello world!"
);
}
// export (expose) foo to other modules
exports
.
foo
=
foo
;
exports
// Import the module containing the foo function
var
exampleModule
=
require
(
"./example-10-9"
);
// Consume the 'foo' function from the imported module
exampleModule
.
foo
();
In Example 10-8, we first import the module containing the foo
function from Example 10-7 using the require()
function. Then, we consume the foo
function by calling it from the imported module with exampleModule.foo()
.
// CommonJS module getting started
// AMD-equivalent of CommonJS example
// AMD module format
define
(
function
(
require
){
var
lib
=
require
(
"package/lib"
);
// some behavior for our module
function
foo
(){
lib
.
log
(
"hello world!"
);
}
// export (expose) foo for other modules
return
{
foobar
:
foo
};
});
This can be done as AMD supports a simplified CommonJS wrapping feature.
app.js:
var
modA
=
require
(
"./foo"
);
var
modB
=
require
(
"./bar"
);
exports
.
app
=
function
(){
console
.
log
(
"Im an application!"
);
}
exports
.
foo
=
function
(){
return
modA
.
helloWorld
();
}
bar.js:
exports
.
name
=
"bar"
;
foo.js:
require
(
"./bar"
);
exports
.
helloWorld
=
function
(){
return
"Hello World!!"
}
The ES module format has become the standard format for encapsulating JavaScript code for reuse, but CommonJS is the default in Node.js. CommonJS modules are the original way to package JavaScript code for Node.js, although starting with version 13.2.0, Node.js has stable support of ES modules.
By default, Node.js treats the following as CommonJS modules:
Files with a .cjs extension
Files with a .js extension when the nearest parent package.json file contains a top-level field type with a value of commonjs
Files with a .js extension when the nearest parent package.json file doesn’t contain a top-level field type
Files with an extension that is not .mjs, .cjs, .json, .node, or .js
Calling require()
always uses the CommonJS module loader, while calling import()
always uses the ECMAScript module loader irrespective of the type value configured in the nearest parent package.json.
Many Node.js libraries and modules are written with CommonJS. For browser support, all major browsers support the ES module syntax, and you can use import/export in frameworks like React and Vue.js. These frameworks use a transpiler like Babel to compile the import/export syntax to require()
, which older Node.js versions natively support. Libraries written using ES6 module syntax will be transpiled to CommonJS under the hood if you run the code in Node.
There are developers who feel that CommonJS is better suited to server-side development, which is one reason there was a disagreement over whether AMD or CommonJS should be used as the de facto standard before ES2015. Some arguments against CommonJS were that many CommonJS APIs address server-oriented features that one would be unable to implement at a browser level in JavaScript—for example, io, system, and js could be considered unimplementable by the nature of their functionality.
Regardless, it’s useful to know how to structure CommonJS modules so that we can better appreciate how they fit in when defining modules that may be used everywhere. Modules with applications on both the client and server include validation, conversion, and templating engines. Some developers approached choosing which format to use by opting for CommonJS when a module can be used in a server-side environment and using AMD or ES2015 if this is not the case.
ES2015 and AMD modules can define more granular things like constructors and functions. CommonJS modules can only define objects, which can be tedious to work with if we’re trying to obtain constructors from them. For new projects in Node.js, ES2015 modules provide an alternative to CommonJS on the server and also ensure that the syntax is identical to the client-side code. Thus, it creates an easier route to isomorphic JavaScript, which can run in the browser or on the server.
Although it’s beyond the scope of this section, you may have noticed that there were different types of require
methods mentioned when discussing AMD and CommonJS. The concern with a similar naming convention is confusion, and the community is split on the merits of a global require
function. John Hann’s suggestion here is that rather than calling it require
, which would probably fail to achieve the goal of informing users about the different between a global and inner require
, it may make more sense to rename the global loader method something else (e.g., the name of the library). It’s for this reason that a loader like curl.js uses curl()
instead of require
.
Both AMD and CommonJS are valid module formats with different end goals.
AMD adopts a browser-first approach to development, opting for asynchronous behavior and simplified backward compatibility, but it doesn’t have any concept of file I/O. It supports objects, functions, constructors, strings, JSON, and many other types of modules, running natively in the browser. It’s incredibly flexible.
CommonJS, on the other hand, takes a server-first approach, assuming synchronous behavior, no global baggage, and attempts to cater to the future (on the server). What I mean by this is that because CommonJS supports unwrapped modules, it can feel a little closer to the ES2015+ specifications, freeing us of the define()
wrapper that AMD enforces. CommonJS modules, however, support objects only as modules.
These solutions could be a little lacking for developers wishing to create modules that can work in browser and server-side environments. To help alleviate this, James Burke, I, and several other developers created Universal Module Definition (UMD).
UMD is an experimental module format that allows the definition of modules that work in both client and server environments with all or most of the popular script-loading techniques available at the time of writing. Although the idea of (yet) another module format may be daunting, we will cover UMD briefly for thoroughness.
We began defining UMD by looking at the simplified CommonJS wrapper supported in the AMD specification. Developers wishing to write modules as if they were CommonJS modules could use the following CommonJS-compatible format:
define
(
function
(
require
,
exports
,
module
){
var
shuffler
=
require
(
"lib/shuffle"
);
exports
.
randomize
=
function
(
input
){
return
shuffler
.
shuffle
(
input
);
}
});
It’s essential, however, to note that a module is really only treated as a CommonJS module if it doesn’t contain a dependency array and the definition function contains one parameter at minimum. This also won’t work correctly on some devices (e.g., the PS3). For further information about the wrapper, see the RequireJS documentation.
Taking this further, we wanted to provide several different patterns that worked with AMD and CommonJS and solved typical compatibility problems developers wishing to develop such modules had with other environments.
One such variation we can see next allows us to use CommonJS, AMD, or browser globals to create a module.
Define a module commonJsStrict
, which depends on another module called b
. The filename implies the module’s name, and it’s best practice for the filename and the exported global to have the same name.
If the module b
also uses the same boilerplate type in the browser, it will create a global .b
that is used. If we don’t wish to support the browser global patch, we can remove the root
and pass this
as the first argument to the top function:
(
function
(
root
,
factory
)
{
if
(
typeof
exports
===
'object'
)
{
// CommonJS
factory
(
exports
,
require
(
'b'
)
);
}
else
if
(
typeof
define
===
'function'
&&
define
.
amd
)
{
// AMD. Register as an anonymous module.
define
(
[
'exports'
,
'b'
],
factory
);
}
else
{
// Browser globals
factory
(
(
root
.
commonJsStrict
=
{}),
root
.
b
);
}
}(
this
,
function
(
exports
,
b
)
{
//use b in some fashion.
// attach properties to the exports object to define
// the exported module properties.
exports
.
action
=
function
()
{};
}));
The UMD repository contains variations covering modules that work optimally in the browser, those best for providing exports, those optimal for CommonJS runtimes, and even those that work best for defining jQuery plug-ins, which we will look at next.
UMD provides two patterns for working with jQuery plug-ins: one that defines plug-ins that work well with AMD and browser globals and another that can also work in CommonJS environments. jQuery is not likely to be used in most CommonJS environments, so keep this in mind unless we’re working with an environment that does play well with it.
We will now define a plug-in composed of a core and an extension to that core.
The core plug-in is loaded into a $.core
namespace, which can then be easily
extended using plug-in extensions via the namespacing pattern. Plug-ins loaded
via script
tags automatically populate a plugin
namespace under core
(i.e., $.core.plugin.methodName()
).
The pattern can be nice to work with, because plug-in extensions can access properties and methods defined in the base or, with a bit of tweaking, override default behavior so that it can be extended to do more. A loader is also not required to make any of this fully functional.
For more details of what is being done, please see the inline comments in these code samples.
usage.html:
<
script
type
=
"text/javascript"
src
=
"jquery.min.js"
><
/script>
<
script
type
=
"text/javascript"
src
=
"pluginCore.js"
><
/script>
<
script
type
=
"text/javascript"
src
=
"pluginExtension.js"
><
/script>
<
script
type
=
"text/javascript"
>
$
(
function
(){
// Our plug-in "core" is exposed under a core namespace in
// this example, which we first cache
var
core
=
$
.
core
;
// Then use some of the built-in core functionality to
// highlight all divs in the page yellow
core
.
highlightAll
();
// Access the plug-ins (extensions) loaded into the "plugin"
// namespace of our core module:
// Set the first div in the page to have a green background.
core
.
plugin
.
setGreen
(
"div:first"
);
// Here we're making use of the core's "highlight" method
// under the hood from a plug-in loaded in after it
// Set the last div to the "errorColor" property defined in
// our core module/plug-in. If we review the code further down,
// we can see how easy it is to consume properties and methods
// between the core and other plug-ins
core
.
plugin
.
setRed
(
"div:last"
);
});
<
/script>
pluginCore.js:
// Module/plug-in core
// Note: the wrapper code we see around the module is what enables
// us to support multiple module formats and specifications by
// mapping the arguments defined to what a specific format expects
// to be present. Our actual module functionality is defined lower
// down, where a named module and exports are demonstrated.
//
// Note that dependencies can just as easily be declared if required
// and should work as demonstrated earlier with the AMD module examples.
(
function
(
name
,
definition
){
var
theModule
=
definition
(),
// this is considered "safe":
hasDefine
=
typeof
define
===
"function"
&&
define
.
amd
,
hasExports
=
typeof
module
!==
"undefined"
&&
module
.
exports
;
if
(
hasDefine
){
// AMD Module
define
(
theModule
);
}
else
if
(
hasExports
)
{
// Node.js Module
module
.
exports
=
theModule
;
}
else
{
// Assign to common namespaces or simply the global object (window)
(
this
.
jQuery
||
this
.
ender
||
this
.
$
||
this
)[
name
]
=
theModule
;
}
})(
"core"
,
function
()
{
var
module
=
this
;
module
.
plugins
=
[];
module
.
highlightColor
=
"yellow"
;
module
.
errorColor
=
"red"
;
// define the core module here and return the public API
// This is the highlight method used by the core highlightAll()
// method and all of the plug-ins highlighting elements different
// colors
module
.
highlight
=
function
(
el
,
strColor
){
if
(
this
.
jQuery
){
jQuery
(
el
).
css
(
"background"
,
strColor
);
}
}
return
{
highlightAll
:
function
(){
module
.
highlight
(
"div"
,
module
.
highlightColor
);
}
};
});
pluginExtension.js:
// Extension to module core
(
function
(
name
,
definition
)
{
var
theModule
=
definition
(),
hasDefine
=
typeof
define
===
"function"
,
hasExports
=
typeof
module
!==
"undefined"
&&
module
.
exports
;
if
(
hasDefine
)
{
// AMD Module
define
(
theModule
);
}
else
if
(
hasExports
)
{
// Node.js Module
module
.
exports
=
theModule
;
}
else
{
// Assign to common namespaces or simply the global object (window)
// account for flat-file/global module extensions
var
obj
=
null
,
namespaces
,
scope
;
obj
=
null
;
namespaces
=
name
.
split
(
"."
);
scope
=
(
this
.
jQuery
||
this
.
ender
||
this
.
$
||
this
);
for
(
var
i
=
0
;
i
<
namespaces
.
length
;
i
++
)
{
var
packageName
=
namespaces
[
i
];
if
(
obj
&&
i
==
namespaces
.
length
-
1
)
{
obj
[
packageName
]
=
theModule
;
}
else
if
(
typeof
scope
[
packageName
]
===
"undefined"
)
{
scope
[
packageName
]
=
{};
}
obj
=
scope
[
packageName
];
}
}
})(
"core.plugin"
,
function
()
{
// Define our module here and return the public API.
// This code could be easily adapted with the core to
// allow for methods that overwrite and extend core functionality
// in order to expand the highlight method to do more if we wish.
return
{
setGreen
:
function
(
el
)
{
highlight
(
el
,
"green"
);
},
setRed
:
function
(
el
)
{
highlight
(
el
,
errorColor
);
}
};
});
UMD doesn’t aim to replace AMD or CommonJS but merely offers some supplemental assistance for developers wishing to get their code working in more environments today. For further information or to contribute suggestions toward this experimental format, see this GitHub page.
This section reviewed several options for writing modular JavaScript using different module formats before ES2015+.
These formats had several advantages over using the Module pattern alone, including avoiding the need to manage global variables, better support for static and dynamic dependency management, improved compatibility with script loaders, better compatibility for modules on the server, and more.
To conclude our discussion of classic design and architecture patterns, I want to touch on one area where we can apply patterns to structure and organize our JavaScript code in the next chapter on Namespacing patterns.