10 Sep 2012 Javascript Module Loading + Shared Namespaces = Badness
Both module loaders and shared namespaces are valid techniques for modularising your JavaScript code. However, doing both at the same time is a recipe for difficult-to-track defects.
On a recent large-scale JavaScript development project, we were open to different strategies for modularising our code. The version of Backbone Boilerplate that we bootstrapped the project from supported require.js, but also seemed to encourage a shared namespace. Furthermore, Backbone itself used global namespacing, having previously eschewed AMD. So we thought to ourselves: lets use both a module loader and shared namespaces. How bad could it be?
Not good, as it turns out. I’ll give you an example which uses AMD modules.
Say you have a namespace.js
module, which does nothing other than provide a place to put stuff:
// namespace.js define([], function() { return {}; });
And you then have another module a.js
that adds stuff to the namespace, and then returns the namespace:
// a.js define([ "namespace" ], function(namespace) { namespace.a = "I'm module A"; return namespace; });
And a module b.js
that imports a.js
:
// b.js define([ "a" ], function(namespace) { ... return ...; });
All good so far. But say that we also have a module c.js
that imports namespace.js
and b.js
, but forgets to import a.js
– even though it actually also uses the namespace.a
property:
// c.js define([ "namespace", "b" ], function(namespace, b) { console.log(namespace.a); return ...; });
If we were to execute this module, it’ll output to the console:
I'm module A
It worked because b.js
imported a.js
. However, if you were to tweak b.js
so that it didn’t import a.js
anymore:
// b.js define([], function() { // Don't use 'a' anymore return ...; });
Then c.js
will suddenly stop working as expected, because a.js
hasn’t been called by the time it executes, with the result that namespace.a
is undefined
.
That mightn’t sound too bad, but it gets more complicated if b.js
imported another module, which in turn imported a.js
. Or b.js
imported another module, which in turn imported another module, which…well, I think you get the picture. Any changes to your dependency tree can result in things in seemingly unrelated places suddenly becoming undefined
.
I want to emphasize here that it’s shared namespacing that’s the problem, not namespacing full-stop. Namespacing within the object that is returned by a module is still a useful and safe strategy. For example:
define([], function() { var moduleNamespace = {}; moduleNamespace.x = ...; moduleNamespace.y = ...; return moduleNamespace; });
And nor am I saying that you should can get rid of global namespaces entirely. Indeed, it’s likely that you’ll be using libraries that add to a global namespace – for example, Backbone plugins. If you’re using a module loader, you’ll have to wrap these libraries in shim modules (if you were using require.js 1.x, you’d use the use.js plugin; with require.js 2.x, you’d use the built-in shim config). In such cases, it’s important to make clear in your code that such modules, when loaded, will be adding to a global namespace as a side-effect.
Undoing the Mess
So what if you’ve already gotten yourself into this sort of situation, and you need to get out? Well, having spent a couple of days having to back it out of a project myself, here’s three steps I’d suggest:
1. Firstly, make sure that each of your modules returns an object that is at the root of everything created by the module. Do NOT return the namespace that the module adds to. For example, instead of doing this:
// a.js define(['namespace'], function(namespace) { namespace.a = "I'm A!"; return namespace; });
you should do this:
// a.js define(['namespace'], function(namespace) { namespace.a = "I'm A!"; return namespace.a; });
If your module adds several objects to the namespace, then consolidate them underneath a single, root object. So the following:
// x.js define(['namespace'], function(namespace) { namespace.y = "I'm Y!"; namespace.z = "I'm Z!"; return namespace.y; });
would become:
// x.js define(['namespace'], function(namespace) { namespace.x = {y: "I'm Y!", z: "I'm Z!"}; return namespace.x; });
If this seems a bit ugly, hang in there, because it’ll get cleaned-up in a second.
2. Next, convert your modules to always use the value returned by a dependency, rather than a namespaced value. So this:
// b.js define(['namespace', 'a'], function(namespace) { console.log(namespace.a); return ...; });
should become:
// b.js define(['a'], function(a) { console.log(a); return ...; });
3. Finally, stop using the shared namespace entirely. So this:
// a.js define(['namespace'], function(namespace) { namespace.a = "I'm A!"; return namespace.a; });
becomes:
// a.js define([], function() { var a = "I'm A!"; return a; });
or in the more complex case:
// x.js define(['namespace'], function(namespace) { namespace.x = {y: "I'm Y!", z: "I'm Z!"}; return namespace.x; });
becomes:
// x.js define([], function() { var x = {y: "I'm Y!", z: "I'm Z!"}; return x; });
This brings you back to a land of self-contained modules and sanity. It’s not rocket-science, but it’s much easier to track what’s going on.
At the end of the day, I guess all of this is just another variation on an old theme: how global variables are bad. In short, whilst shared namespaces may be an acceptable lowest-common denominator for modularising JavaScript, doing it in conjunction with a module loader puts you at risk of difficult-to-track problems. You have been warned.
SutoCom
Posted at 21:03h, 10 SeptemberReblogged this on Sutoprise Avenue, A SutoCom Source.