Skip to content
bebraw edited this page Apr 15, 2013 · 2 revisions

Believe it or not, JavaScript doesn't come with a module system of its own. ES6 is supposed to fix this problem once and for all. Even though there isn't a native module system yet, it doesn't mean it's not possible to write modular JavaScript. In fact there are multiple solutions each with their strengths and weaknesses.

The Anti-Pattern

Before getting any further, consider the anti-pattern below. This is how people wrote JavaScript in the golden 90s. And they still write at times.

<script src='jquery-current.js' type='text/javascript'></script>
<script src='sorttable.js'      type='text/javascript'></script>
<script src='scripts.js'        type='text/javascript'></script>
<script src='miniCalendar.js'   type='text/javascript'></script>

The main problem with this approach is that it really doesn't scale as your application does. Can you repeat after me "it's brittle"? Especially in team environment using this sort of approach is just asking for trouble.

Concatenating to Victory

So what does a clever programmer do next? Concatenates. That's what a clever programmer does. Instead of having multiple scripts you will have just one entry point to your application like below.

<script src='application.js' type='text/javascript'></script>

It is possible to merge this approach with above one especially if you want to load some resources from a CDN such as jsDelivr. The advantage of doing this is that popular libraries, such as jQuery, can get loaded very fast as they are very likely in the browser cache already. As the old saying goes "Use the cache, Luke".

The approach isn't without its problems. Someone still has to write that concatenation script. And you probably want to load things in the right order or otherwise bad things™ will happen.

AMD

The next logical step would be to make sure things get loaded in the right order and write a tool that happens to do that. To do this we'll need to define our dependencies somehow and be able to parse them in a meaningful order. Because this is how module systems roll.

One popular way to achieve this goal on definition level is to use AMD (not Intel). AMD stands for Asynchronous Module Definition. You can see a simple AMD module below:

// this is a collection of some widgets
define(['jquery', 'math'], function($, math) {
    // operate using $, math
    function range() {
        ...
    }

    ...

    // exports
    return {
        xRange: function() {
            ... 
        },
        yRange: function() {
            ...
        }
    };
});

The definition consists of a few parts. First of all there's that define wrapper. It is a function that accepts a list and a function in this case. Dependencies of the module are defined within that list. They will appear as parameters to that callback. The callback can then operate using those dependencies. Finally it may return exports. That is the public interface the module provides.

define may also accept just a function or an object. You can find examples of these two cases below:

define(function() {
    return function() {
        console.log('Hello mon!');
    };
})

// or just Object (handy for configuration)
define({
    showDebugTools: true,
    features: {
        newRange: true,
        newUi: true
    }
});

Even though the approach leads to some overhead on module definition level you have that dependency information now. This is something RequireJS optimizer may use to produce that concatenated version should you want to. It may also minify the code for you. In this process the code will be mangled into a minimal form (ie. no spaces, short variable names and such). The code won't be very debuggable this way but at least it will be compact.

RequireJS comes with some nice features such as asynchronous loading and package definition. Asynchronous loading enables the browser to load and execute dependencies at the same time. So rather than loading a single, big file once and then starting to parse it, it can operate piece-wise instead.

Package definition is one of those features that allows you to split your functionality further. In fact using it you may end using micro modules. A micro module is a module that provides just one function. It doesn't get simpler than that. This is a great thing for building libraries. Mout and funkit are examples of libraries using this particular approach.

As not to make things too simple I'll show you one more trick. It is possible to mix AMD and CommonJS definitions. I've ported that first AMD example above to the scheme. Compare and contrast.

// this is a collection of some widgets
define(function(require, exports) {
    var $ = require('$');
    var math = require('math');

    // operate using $, math
    function range() {
        ...
    }

    ...

    // exports
    exports.xRange = function() {
        ... 
    };
    
    exports.yRange = function() {
        ...
    };
});

At least to my eyes that's more readable. But that might have something to do with the fact that I've been using Node too much.

CommonJS

Porting our little example to CommonJS scheme that Node.js implements isn't that hard. We just need to remove something. Consider following:

var $ = require('$');
var math = require('math');

// operate using $, math
function range() {
    ...
}

...

// exports
exports.xRange = function() {
    ... 
};
    
exports.yRange = function() {
    ...
};

It doesn't get prettier than that. I personally like this definition a lot. And if you ever use Node, you will bump into it. It isn't just a Node thing anymore, though.

Browserify allows you to use the definition on browser side as well. It comes with a tool that interprets the dependency information and constructs a file which you can then load like above. For some this is the holy grail as it allows you to share some code between the backend and the frontend. It would not surprise me if the approach gained some popularity.

UMD

As a library author it isn't unfortunately enough to stick to just one module definition (or plain old global). Especially if you wish to reach to a larger possible pool of users. This is where something ugly but practical known as UMD comes in.

It is a collection of patterns that allow you to support multiple module systems at once depending on your needs. It's so ugly I rather spare you from the details. Go and check it yourself if you dare.

ES6

ES6 should finally come with a module system. About time! It is actually possible to give the proposed approach a go already by using a transpiler. Unfortunately the definition doesn't quite support all possible use cases well.

For instance it isn't possible to export a single function. This is a pattern you tend to see especially in the Node world. And micro modules require support for this sort of export. I really hope they fix this little omission. This would make it possible for me to write some of my library code using the definition and then transpile it to wanted targets (likely AMD and CommonJS).

Package Managers

Having a module definition is just a part of the story. What's the point of having modules if you cannot share them, or better yet, use modules written by other people? This is where various package managers come in. On Node side of the fence the answer is simple, just use NPM. On frontend side it becomes a bit trickier.

In case you use Browserify you can still use NPM. Problem solved! If you don't want to use NPM for some reason, there are plenty of alternatives. In practice I've found Bower quite cool. They have a nice site available for browsing packages even. component is yet another alternative. With a bit of digging you'll find plenty of more.

Package Definition

Each of these systems will use some kind of a package definition. It is usually a JSON file which describes package metadata including dependencies. The package manager tool will be able to install the right dependencies based on this information.

A word of warning! It is a very good idea to fix your dependencies to certain versions. This way the next you have to set up your application it is more likely to work than not. On very special occasions versions of libraries may have disappeared from the server (happened to me once). It is a good idea to prepare for this continuity as well. If you use Node, consider using a package such as lockdown. Paranoia can be useful when it comes to this sort of things.

Finding Modules

It can be a little bit hard to find modules you are after, especially on the frontend side. Besides NPM, Node guys might want to check out node-toolbox. It provides an alternative view to NPM data. On the frontend side you might want to consider JSwiki, JSter or JSDB for instance. They are a bit different takes on the same subject. You'll find a few more over at JSwiki.

Conclusion

I hope this chapter gave you some idea how to work with modules in JavaScript. The situation will likely change in the coming years especially on the frontend side. Even when ES6 type modules become supported on browser level some form of transpiling will have to be used to support older ones.

That said I think the approach you choose depends largely on your needs. AMD is quite alright for organizing larger projects. Its configuration isn't entirely trivial and sometimes you might run into issues with certain dependencies. Not all modules work that well with it by default. Those are more of special cases, though.

Personally I am very thrilled about the approach chosen by Browserify. It just feels, or at least looks, right. Again, its usefulness likely depends on type of a project.