You are reading a translation of an old blog post published on my previous blog in French.
Whilst native solutions to these problems will be arriving in ES Harmony, the good news is that writing modular JavaScript has never been easier and you can start doing it today.
— Addy Osmani, creator de Yeoman
The benefits of a modular design are obvious. Sadly, before the version ECMAScript 6, JavaScript provided no support to define modules. This was before AMD.
AMD (Asynchronous Module Definition) defines a format to define modules in JavaScript. Several script loaders already implement this standard. The most popular is RequireJS.
Using RequireJS, we will write scoped modules, without polluting the global namespace, with the additional benefit to declare explicitly module dependencies. From an implementation viewpoint, RequireJS is an extension of the pattern Module, widely implemented in JavaScript.
RequireJS is published under the new BSD license and MIT. The code presented in this article has been simplified for obvious reasons and must not be used outside this learning context. This article is based on the latest version of RequireJS (2.1.16) at the moment of publication.
Getting Started with Modules
AMD defines two main methods:
define
to define a new module.require
to define and load dependencies.
Let’s start by the method define
:
The first parameter is used to name the module. Often, modules will be anonymous (a best practice to ease their reorganization). Then comes the list of dependencies to load, that will be passed as the last parameter, the function responsible to really instantiate the module.
Here is an example:
require
is most often used to define the entry point (the first file loaded by RequireJS) or inside the definition of a module as follows:
We will not go further in the presentation of RequireJS/AMD. Read the official API documentation for more details.
A First Example
On loading, RequireJS inspects the special attribute data-main
to find the first application module to load. This is the file main.js
present under the directory scripts/
:
This file contains a simple call to the function require
to request the loading of another module (helper/util
) defining the function toFunnyCase
. The remaining lines simply update the title of the HTML page using this mysterious toFunnyCase
function.
So, let’s inspect this file scripts/helper/util.js
:
The module is anonymous, without any dependencies, defining a unique function producing the following result when our page is loaded in our browser:
Our goal is now to remove the dependency on require.js, and to provide a custom implementation that we will write step by step.
RequireJS, Under the hood
Before jumping headfirst into the code, let’s take a look at the HTTP requests sent by RequireJS on our example.
require.js
inspects the attributedata-main
to determine the first file to load, in our case,main.js
. A first Ajax request is emitted.main.js
calls the functionrequire
. One dependency is declared. The methodrequire
triggers a second Ajax request to downloadutil.js
.util.js
does not have dependencies. The instantiation callback of this module is executed. RequireJS memorizes the result for the next step.- We are back in the file
main.js
. All dependencies have been loaded. The instantiation callback of this module is finally executed, receiving the previous module result in argument.
Let’s Go
We start by modifying the example file:
- 1
- We import jQuery, not indispensable but convenient to reuse some utility functions.
- 2
- We replace the library RequireJS with a new file
require.lite.js
that we will be the subject of this article.
Here is the skeleton of the file require.lite.js
:
The code starts by inspecting the attribute data-main
:
We split the attribute value to extract the dirname (baseUrl
) from the basename (mainScript
). This directory will be used as the base directory when loading other scripts. The code ends by calling the method require
. Time has come to get to the heart of RequireJS.
Module
RequireJS relies heavily on the object Module
whose constructor is defined like this:
- 1
depExports
will contain the arguments that will be passed to the instantiation callback of this module. After the loading of a dependency, we save the value in this array.- 2
- The instantiation callback must execute only when all its dependencies have been loaded. Using this counter, we know the number of dependencies that are still not completely loaded.
- 3
- It is important to assign an id even for simple files containing a simple call to
require
. If this file is referenced several times, it will be loaded only once. - 4
- We implement the pattern Observer. Other modules can register to watch the progression. In practice, we are going to use it only to know when a module has been defined. (RequireJS generates a lot more events internally that are not useful for our basic example). What follows are two utility methods used to support this use case.
The object Module
exists but nothing happens. It’s only when the method init
is called that the magic happens, and precisely during the execution of the method enable
.
- 1
- As for the method
init
, we memorize that the module has been activated. It prevents the module from being initialized twice. - 2
- We load dependencies transitively. We can enable the module before all its dependencies have been enabled first. We iterate over them, and if the dependency is new (this is the motivation behind the variable
registry
), we instantiate itsModule
, before trying its activation (recursive method). - 3
- The property
depCount
is incremented to indicate that we are waiting for the loading of this module. - 4
- We register to decrement this variable when the module has been defined.
- 5
- There are two calls to the method
check
: at the end of our own activation, and for each dependency definition. Here is the code of this methodcheck
:
This method check
tries to finalize the module (i.e., execute the callback). We start by checking if the module has already been initialized, in which case we just have to request its loading (= Ajax request). Otherwise, we try the method define
.
define
checks that all dependencies have been loaded correctly (using the property depCount
). If every condition is satisfied, depExports
is passed to the instantiation callback. Done! We publish a new event to propagate the news to other modules, which as we have mentioned before, listen attentively for this event, to try to call the method check
themselves to finalize their own definition.
Several solutions exist, but the most widespread is to append a new tag <script>
in the DOM (under <head>
for example). It’s the technique used by RequireJS:
We should now implement the two main methods defined by AMD. Using the object Module
.
require
The definition of the method require
is trivial:
We just have to create a new module that is initialized immediately. Its dependencies will be initialized transitively.
define
The method define
is not obvious to implement, but neither too complicated.
Let’s take the example file main.js
.
When this script is executed, we have seen that the method require
triggers the loading of all dependencies (using the method enable
). The script util.js
is then executed:
We arrive in the method define
, but we ignore the name of the module. So how can we finish its activation? What is the name to use to register the result?
The solution implemented by RequireJS is to memorize the arguments of every execution of the method define
(in a queue, as several modules can be loaded simultaneously):
- 1
- Not a string in first argument, we know this is an anonymous module, we shift the arguments in consequence.
- 2
- Not an array, we know that the module has no dependency. We shift the arguments again.
- 3
- Variables contains now the right value.
We modify the method loading a script to declare a new callback. This function will be executed just after the method define
, the perfect moment to reread previously saved information and to end the module instantiation.
- 1
- We trigger the callback on the event
load
. - 2
- We traverse saved values until finding our module.
- 3
- We complete the module initialization.
Congratulations, our minimal rewrite of RequireJS is now complete. Less than 300 lines have been required to make our example works again. The complete source code is available here.
- We can load a JavaScript file dynamically using a new tag
script
, usinghead.appendChild()
. - RequireJS is a good example of a concept called “Programming into a language” compared to “Programming in a language” (Code Complete, Steve McConnell): “Programmers who program “in” a language limit their thoughts to constructs that the language directly supports. … Programmers who program “into” a language first decide what thoughts they want to express, and then determine how to express those thoughts using the tools provided by their specific language.”
- All libraries are not defined as AMD modules. Many continue to update the global namespace in JavaScript but all is not lost. RequireJS supports shim to configure explicitly the dependencies of these librairies.
- The modules RequireJS do not pollute the global namespace so that we can load several versions of the same library. Hint: RequireJS supports several contexts.
- RequireJS offers also an optimizer, the aim of which is to group several modules, and minify them together, etc., to reduce the number of Ajax requests. How does it work?
- RequireJS supports the syntax defined by CommonJS using a simplified wrapper to get this result:
How does dependency injection work without the array of dependencies in arguments? Hint: Function.prototype.toString()
.