Javascript wokflow for dummies - require.js

This is the part 2 of the Javascript workflow series.

If you've ever made something big, you probably ended up with a big javascript file of 1XXX lines. Or maybe you tried to separate things in multiple files, in which case you probably ran into some problems. There is a solution for keeping all your code in little files, and this solution is require.js.

The problem when loading scripts

So when you create something that uses javascript, it runs in the browser. What it means is that your scripts first need to be downloaded by the user. So let's take this example page:

<html>  
    <head>
        <title>Page</title>
    </head>
    <body>
        <script src="littleScript.js"></script>
        <script src="bigScript.js"></script>
    </body>
</html>  
// in littleScript.js
console.log(magicVariable);

// in bigScript.js
var magicVariable = 10;

So, imagine bigScript is filled with tons of other codes and is a bit long to load. What happens?

  • The browser starts loading littleScript and bigScript
  • littleScript has finished loading, so it is executed
  • But wait, there is no magicVariable, so the console.log(magicVariable); shows `undefined``
  • bigScript has finished loading, so it is executed. magicVariable is set, but it's too late. Oh snap.

Another type of problem you may encounter (imagine firstScript and secondScript are both loaded without problems):

// in firstScript.js
var player = {  
    x : 5,
    y : 10,
    name : "player",
    strength : config.defaultStrength
};

// in secondScript.js
var config = {  
    defaultStrength : 10,
    defaultName : player.name
};

See the big problem here? Let's look at what the JS will do:

  • let's declare the player variable. So, x is 5, y is 10, name is "player", and strength is... Oh, I need to check config
  • so, config.defaultStrength is... Oh, I need to check player
  • wait, what?

And at this point, javascript usually insults you about something called circular references. What happens is that two objects are referring to each other to be created, so it's an egg and chicken problem: Impossible to answer.

There are many other problematic situations caused by loading scripts manually so I won't go over them all but you get the idea.

Require, our savior

Luckily some guys already thought of ways to manage these sort of problems properly, so let me introduce you to require.js.

Require is a script loader and dependencies manager. What it means is that this thing will take care of loading scripts for you.

When using require.js, each of your files is wrapped in a define scope, that tells javascript which modules (files) you need as dependencies, and loads them before launching your file.
What it means is that instead of throwing your scripts in a page and hoping it will go well, you now have wrapped module that will wait for their dependencies to be loaded properly, so you can be sure everything gets executed in the order you want it. Here is an example:

<html>  
<head>  
    <title>Require js stuff</title>
</head>  
<body>  
    <script src="require.js" data-main="game"></script>
    <canvas id="myCanvas"></canvas>
</body>  
</html>  
// config.js
define ([], function () {  
    var config = {
        gameWidth : 800,
        gameHeight : 600,
        playerName : "God"
    };

    return config;
});

// canvas.js
define (["config"], function (config) {  
    var canvas = document.getElementById('myCanvas');
    canvas.width = config.gameWidth;
    canvas.height = config.gameHeight;

    return canvas;
});

// game.js
define (["canvas", "config"], function (canvas, config) {  
    var ctx = canvas.getContext('2d');
    ctx.font = "12px Arial";
    ctx.fillStyle = "red";
    ctx.fillRect(config.gameWidth / 2, config.gameHeight / 2, 20, 20);
    console.log(ctx);
    ctx.fillText(config.playerName, config.gameWidth / 2, config.gameHeight / 2);
});

So let's explain a little what happens here, starting from game.js:

game.js needs to have access to the canvas and the config, so it requires these two dependencies. To do so, it passes an array to the define call with them, and a function that will be called once loaded, with the wanted modules as parameters.

The syntax of the define function goes like this:

define (dependencies, callback);

dependencies is an array containing the file names (minus the .js extension), and callback is a function that will be called once those files are loaded.

Ok, and what is the callback function for?

The callback function contains your module (file) code. That is, all code of this file will be contained in this function, so that it is wrapped in a nice little scope.

Meanwhile, require.js will load the scripts you want, and once loaded will call your function, with the scripts as parameters. This is why our define call looks like this in game.js:

define (["config", "canvas"], function (config, canvas) {  
    // This is the main code for this file, the inside of the callback function
    // It gets as parameters config and canvas
    // Then it can run code using both of them
});

Right. And how does require.js know what are the values of config and canvas?

With require, modules are basically functions. So every module you create can return a value, which is what require will send to other modules requiring it. In our case:

function () {  
    config = {
        //...
    };

    return config;
}

This is the code of our config module. A function returning an object. So everytime a module will require config by doing this:

define (["config"], function (config) {  
    // do stuff
});

it will actually send the value returned by the function in config.js.

So now that we understand what require.js does, let's see what happens in our little program:

<html>  
<head>  
    <title>Require js stuff</title>
</head>  
<body>  
    <script src="require.js" data-main="game"></script>
    <canvas id="myCanvas"></canvas>
</body>  
</html>  
// config.js
define ([], function () {  
    var config = {
        gameWidth : 800,
        gameHeight : 600,
        playerName : "Player"
    };

    return config;
});

// canvas.js
define (["config"], function (config) {  
    var canvas = document.getElementById('myCanvas');
    canvas.width = config.gameWidth;
    canvas.height = config.gameHeight;

    return canvas;
});

// game.js
define (["canvas", "config"], function (canvas, config) {  
    var ctx = canvas.getContext('2d');
    ctx.font = "12px Arial";
    ctx.fillStyle = "red";
    ctx.fillRect(config.gameWidth / 2, config.gameHeight / 2, 20, 20);
    console.log(ctx);
    ctx.fillText(config.playerName, config.gameWidth / 2, config.gameHeight / 2);
});
  • The require.js script is loaded. It looks for the data-main attribute of its script tag to know which file to start from.
  • The game module is loaded, that is game.js
  • game has two dependencies: canvas and config. Require starts loading them.
  • canvas has a dependency: config. Require waits for it to be loaded.
  • config is loaded. Its function is executed, and it returns the config object.
  • config being loaded, canvas can be executed. It sets the canvas width and height using the values returned by config, and returns the canvas
  • canvas and config are both loaded, game can be executed. It takes the canvas and writes a value of the config module into it.
  • The program is finished

You can see it running here

Using require.js in a project

Using require.js is pretty simple. First, download it and put it somewhere in the folder of your project.

Then, like in my example, add a script tag in your <head>:

<script type="text/javascript" src="require.js" data-main="nameOfMyMainFile"></script>  

Once this is done, just write all your files wrapped in define calls as explained before, and you won't have to think of how scripts are loaded anymore.

The circular dependencies

There is one last thing I should warn you about when using require.js, it is circular dependencies.

While require is able to help you track exactly which scripts need what, it is not magical and some things are just impossible. Look at the require version of one previous example:

//player.js
define (["config"], function (config) {  
    var player = {
        x : 5,
        y : 10,
        name : "player",
        strength : config.defaultStrength
    };
    return player;
});

//config.js
define (["player"], function (player) {  
    var config = {
        defaultStrength : 10,
        defaultName : player.name
    };

    return config;
});

You will have the same problem as before: Two scripts are trying to use each other. Basically require will wait indefinitely for them to load. It won't solve it magically.

But what require does is help you spot these sort of error. Now that you can see on top of each file what it needs, it is waaaaay easier to see when you have these sort of problems.

And when that happens you will be very pissed and want to throw require.js away, but the thing is that without require it would be even worse. When you have circular dependencies it is an error of design, and you should think of another way to organize your script dependencies.

In our example the obvious answer is to set config.defaultName to a fixed value, and player.name to config.defaultName. Then we can remove player from the dependencies of config, since it has no sense at all for our game configuration to depend on the player.

Conclusion

Require helps you think in terms of modules and dependencies. It helps you with:

  • Understanding relations between pieces of your code
  • Being able to see the order in which your program flow (by going module to module)
  • Tracking dependencies easily, and spotting circular references before they make your life a hell
  • Being able to have multiple people working on different modules and letting require coordinate everything

So basically, if in any project you plan to write more than a few javascript lines, take a minute to add require.js and put your code in modules.


comments powered by Disqus