Optional loading of RequireJS modules
Posted on Tue 29 September 2015 in Code
RequireJS is a module loader for JavaScript. Similar to its alternatives such as
Browserify, it tries to solve an important problem on the web front(end):
dividing JavaScript code into modules for better maintainability while still loading them correctly and efficiently
without manual curation of the <script>
tags.
Once it’s configured correctly (which can be rather non-trivial, though),
modules in RequireJS are simply define
d as functions that return arbitrary JavaScript objects:
define([
'jquery',
'lodash',
'myapp/dep1',
'myapp/dep2',
], function($, _, dep1, dep2) {
// ... all of the module's code ...
return {
exportedSymbol1: ...,
exportedSymbol2: ...,
};
});
Before executing the function, RequireJS loads all the specified dependencies, repeating the process recursively
and asynchronously. Return values from module functions are passed as parameters to the next module function,
and thus the whole mechanism clicks, serving as a crafty workaround for the lack of proper import
functionality1.
Relative failures
If, at some point in the chain, the desired module cannot be found or loaded, the entire process grinds to a halt
with an error. Most of the time, this is perfectly acceptable (or even desirable) behavior, equivalent to an incorrect
import
statement, invalid #include
directive, or similar mistake in other languages.
But there are situations when we’d like to proceed with a missing module, because the dependent code is prepared
to handle it. The canonical example are
Web Workers.
Unlike traditional web application code, Web Worker scripts operate outside of a context of any single page,
having no access to the DOM tree (because which DOM tree would it be?).
Correspondingly, they have no document
nor window
objects in their global scope.
Unfortunately, some libraries (*cough* jQuery *cough*) require those objects as a hard (and usually implicit)
dependency. This doesn’t exactly help if we’d like to use them in worker code for other features, not related to DOM.
In case of jQuery, for example, it could be the API for making AJAX calls, which is still decidedly more pleasant
than dealing with bare XMLHTTPRequest
if we’re doing anything non-trivial.
Due to this hard dependency on DOM, however, Web Workers cannot require
jQuery. No biggie, you may think: browsers
supporting workers also offer an excellent, promise-based
Fetch API that largely replaces the old AJAX,
so we may just use it in worker code. Good thinking indeed, but it doesn’t solve the issue of sharing code between
main (“UI”) part of the app and Web Workers.
Suppose you have the following dependency graph:
The common
module has some logic that we’d want reused between regular <script>
-based code and a Web Worker,
but its dependency on jQuery makes it impossible. It would work, however, if this dependency was a soft one.
If common
could detect that jQuery is not available and fall back to other solutions (like the Fetch API),
we would be able to require
it in both execution environments.
The optional
plugin
What we need, it seems, is an ability to say that some dependencies (like 'jquery'
) are optional. They can be loaded
if they’re available but otherwise, they shouldn’t cause the whole dependency structure to crumble. RequireJS does not
support this functionality by default, but it’s easy enough to add it via a plugin.
There are already several useful plugins available for RequireJS that offer some interesting features. As of this writing, however, optional module loading doesn’t seem to be among them. That’s not a big problem: rolling out our own2 plugin turns out to be relatively easy.
RequireJS plugins are themselves modules: you create them as separate JavaScript
files having code wrapped in define
call. They can also declare their own dependencies like any other module.
The only requirement is that they export an object with certain API:
at minimum, it has to include the load
method. Since our optional
plugin is very simple, load
is in fact
the only method we have to implement:
/* Skeleton of a simple RequireJS plugin module. */
define([], function() {
function load(moduleName, parentRequire, onload, config) {
// ...
}
return {
load: load,
};
});
As its name would hint, load
carries out the actual module loading which a plugin is allowed to influence, modify,
or even replace with something altogether different. In our case, we don’t want to be too invasive, but we need to
detect failure in the original loading procedure and step in.
I mentioned previously that module loading is asynchronous, which JavaScript often translates to “callbacks”.
Here, load
receives the onload
callback which we eventually need to invoke. It also get the mysterious
parentRequire
argument; this is simply a regular require
function that’d normally be used if our plugin didn’t
stand in the way.
Those two are the most important pieces of the puzzle, which overall has a pretty succinct solution:
/**
* RequireJS plugin for optional module loading.
*/
define ([], function() {
/** Default value to return when a module failed to load. */
var DEFAULT = null;
function load(moduleName, parentRequire, onload) {
parentRequire([moduleName], onload, function (err) {
var failedModule = err.requireModules && requireModules[0];
console.warn("Could not load optional module: " + failedModule);
requirejs.undef(failedModule);
define(failedModule, [], function() { return DEFAULT; });
parentRequire([failedModule], onload);
});
}
return {
load: load,
};
});
The logic here is as follows:
- First, try to load the module normally (via the outer
parentRequire
call). - If it succeeds,
onload
is called and there is nothing for us to do. - If it fails, we log the
failedModule
and cleanup some internal RequireJS state withrequirejs.undef
. - Most importantly, we
define
the module as a trivial shim that returns someDEFAULT
(here,null
). - As a result, when we require it again (through the inner
parentRequire
call), we know it’ll be loaded successfully.
Usage
Plugins in RequireJS are invoked on a per-module basis. You can specify that a certain dependency 'bar'
shall be loaded
through a plugin 'foo'
by putting 'foo!bar'
on the dependency list:
define([ 'foo!bar'], function(bar) {
// ...
});
Both 'foo'
and 'bar'
represent module paths here: the first one is the path to the plugin module,
while the second one is the actual dependency. In a more realistic example — like when our optional loader is involved —
both of them would most likely be multi-segments paths:
define([
'myapp/ext/require/optional!myapp/common/buttons/awesome-button',
], function(AwesomeButtonController) {
// ...
});
As you can see, they can get pretty unreadable rather quickly. It would be better if the plugin prefix consisted
of just one segment (i.e. optional!
) instead. We can make that happen by adding
a mapping to the RequireJS config:
requirejs.config({
// ...
map: {
'*': {
'optional': 'myapp/ext/require/optional',
}
}
})
With this renaming in place, the loading of non-mandatory dependencies becomes quite a bit clearer:
define([
'optional!myapp/common/buttons/awesome-button',
], function(AwesomeButtonController) {
// ...
if (!AwesomeButtonController) {
// ... (some work around) ...
}
});
Of course, you still need to actually code around the potential lack of an optional dependency.
The if
statement above is just an illustrative example; you may find it more sensible to provide some shim instead:
AwesomeButtonController = AwesomeButtonController || function() {
// ...
};
Either way, I recommend trying to keep the size of such conditional logic to a minimum. Ideally, it should be confined to a single place, or — better yet — abstracted behind a function.
-
An actual
import
statement has made it into the ES6 (ECMAScript 2015) standard but, as of this writing, no browser implements it. ↩ -
Most of the code for the plugin presented here is based on this StackOverflow answer. ↩