Writing modular JavaScript

Before the advent of Node.js, given all of JavaScript's infamous restrictions, probably the biggest complaint that it received from developers was the lack of built-in support for a modular development process.

The best practice for modular JavaScript development was creating your components inside a literal object, which, in its own way, behaved somewhat like a namespace. The idea was to create an object in the global scope, then use the named properties inside that object to represent specific namespaces where you would declare your classes, functions, constants, and so on (or at least the JavaScript equivalent).

var packt = packt || {};
packt.math = packt.math || {};
packt.math.Vec2 = function Vec2(x, y) {// …
};

var vec2d = new packt.math.Vec2(0, 1);
vec2d instanceof packt.math.Vec2; // true

In the previous code snippet, we create an empty object in case the packt variable doesn't exist. In case it does, we don't replace it with an empty object, but we assign a reference to it to the packt variable. We do the same with the math property, inside which we add a constructor function named Vec2d. Now, we can confidently create instances of that specific vector class, knowing that, if there is some other vector library in our global scope, even if it's also named Vec2, it won't clash with our version since our constructor function resides inside the packt.math object.

While this method worked relatively well for a long time, it does come with three drawbacks:

  • Typing the entire namespace every time needs a lot of work
  • Constantly referencing deeply nested functions and properties hurts performance
  • Your code can easily be replaced by a careless assignment to a top-level namespace property

The good news is that today there is a better way to write modules in JavaScript. By recognizing the shortcomings of the old way of doing things, a few proposed standards have emerged to solve this very problem.

CommonJS

In 2009, the folks at Mozilla created a project that was aimed at defining a way to develop JavaScript applications that were freed from the browser. (refer to http://en.wikipedia.org/wiki/CommonJS.) Two distinct features of this approach are the require statement, which is similar to what other languages offer, and the exports variable, from where all the code to be included on a subsequent call to the require function comes. Each exported module resides inside a separate file, making it possible to identify the file referenced by the require statement as well as isolate the code that makes up the module.

// - - - - - - -
// player.js

var Player = function(x, y, width, height) {
   this.x = x;
   this.y = y;
   this.width = width;
   this.height = height;
};

Player.prototype.render = function(delta) {
   // ...
};

module.exports = Player;

This code creates a module inside a file named player.js. The takeaways here are as follows:

  • The contents of your actual module are the same old, plain JavaScript that you're used to and are in love with
  • Whatever code you wish to export is assigned to the module.exports variable

Before we look at how to make use of this module, let us expound on the last point mentioned previously. As a result of how JavaScript closures work, we can reference values in a file (within the file) that are not directly exported through module.exports, and the values cannot be accessed (or modified) outside the module.

// - - - - - - -
// player.js

// Not really a constant, but this object is invisible outside this module/file
var defaults = {
   width: 16,
   height: 16
};

var Player = function(x, y, width, height) {
   this.x = x;
   this.y = y;
   this.width = width || defaults.width;
   this.height = height || defaults.height;
};

Player.prototype.render = function(delta) {
   // ...
};

module.exports = Player;

Note that the Player constructor function accepts a width and height value, which will be assigned to a local and corresponding width and height attribute on instances of that class. However, if we omit these values, instead of assigning undefined or null to the instance's attributes, we fallback to the values specified in the defaults object. The benefit is that the object cannot be accessed anywhere outside the module since we don't export the variable. Of course, if we make use of EcmaScript 6's const declaration, we could achieve read-only named constants, as well as through EcmaScript 5's Object.defineProperty, with the writable bit set to false. However, the point here still holds, which is that nothing outside an exported module has direct access to values within a module that were not exported through module.exports.

Now, to make use of CommonJs modules, we need to be sure that we can reach the code locally within the filesystem. In its simplest form, a require statement will look for a file (relative to the one provided) to include, where the name of the file matches the require statement.

// - - - - - - -
// app.js

var Player = require('./player.js'),
var hero = new Player(0, 0);

To run the script in the app.js file, we can use the following command within the same directory where app.js is stored:

node app.js

Assuming that the app.js and player.js files are stored in the same directory, Node should be able to find the file named player.js. If player.js was located in the parent directory from app.js, then the require statement would need to look like the following:

// - - - - - - -
// test/player_test.js

var Player = require('./../player.js'),
var hero = new Player(0, 0);

As you'll see later, we can use Node's package management system to import modules or entire libraries very easily. Doing so causes the imported packages to be stored in a methodical manner, which, as a result, makes requiring them into your code much easier.

The next way of requiring a module is by simply including the exported module's name in the require statement, as follows:

// - - - - - - -
// app.js

var Player = require('player.js'),
var hero = new Player(0, 0);

If you run the previous file, you will see a fatal runtime error that looks something like the following screenshot:

CommonJS

The reason Node can't find the player.js file is because, when we don't specify the name of the file with a leading period (this means that the file included is relative to the current script), it will look for the file inside a directory named node_modules within the same directory as the current script.

If Node is unable to find a matching file inside node_modules, or if the current directory does not have a directory that is so named, it will look for a directory named node_modules along with a file with the same name, similar to the require statement in the parent directory of the current script. If the search there fails, it will go up one more directory level and look for the file inside a node_modules directory there. The search continues as far as the root of the filesystem.

Another way to organize your files into a reusable, self-contained module is to bundle your files in a directory within node_modules and make use of an index.js file that represents the entry point to the module.

// - - - - - - -
// node_modules/MyPlayer/index.js

var Player = function(x, y, width, height) {
   this.x = x;
   this.y = y;
   this.width = width;
   this.height = height
};

module.exports = Player;

// - - - - - - -
// player_test.js

var Player = require('MyPlayer'),

var hero = new Player(0, 0);
console.log(hero);

Note that the name of the module, as specified in the require statement, now matches the name of a directory within node_modules. You can tell that Node will look for a directory instead of a filename that matches the one supplied in the require function when the name doesn't start with characters that indicate either a relative or absolute path ("/", "./", or "../") and the file extension is left out.

When Node looks for a directory name, as shown in the preceding example, it will first look for an index.js file within the matched directory and return its contents. If Node doesn't find an index.js file, it will look for a file named package.json, which is a manifest file that describes the module.

// - - - - - - -
// node_modules/MyPlayer/package.json

{
   "name": "MyPlayer",
   "main": "player.js"
}

Assuming that we have renamed the node_modules/MyPlayer/index.js file as node_modules/MyPlayer/player.js, all will work as before.

Later in this chapter, when we talk about npm, we will dive deeper into package.json since it plays an important role in the Node.js ecosystem.

RequireJS

An alternative project that attempts to solve JavaScript's lack of native script importing and a standard module specification is RequireJS. (refer to http://requirejs.org/.) Actually, RequireJS is a specific implementation of the Asynchronous Module Definition (AMD) specification. AMD is a specification that defines an API for defining modules such that the module and its dependencies can be asynchronously loaded [Burke, James (2011). https://github.com/amdjs/amdjs-api/wiki/AMD].

A distinctive difference between CommonJS and RequireJS is that RequireJS is designed for use inside a browser, whereas CommonJS doesn't have a browser in mind. However, both methods can be adapted for the browser (in the case of CommonJS) as well as for other environments (in the case of RequireJS).

Similar to CommonJS, RequireJS can be thought of as having two parts: a module definition script and a second script that consumes (or requires) the modules. In addition, similar to CommonJS but more obvious in RequireJS, is the fact that every app has a single entry point. This is where the requiring begins.

// - - - - - - -
// index.html

<script data-main="scripts/app" src="scripts/require.js"></script>

Here, we include the require.js library in an HTML file, specifying the entry point, which is indicated by the data-main attribute. Once the library loads, it will attempt to load a script named app.js that is located in a directory named scripts, which is stored on the same path as the host index.html file.

Two things to note here are that the scripts/app.js script is loaded asynchronously, as opposed to the default way all scripts are loaded by the browser when using a script tag. Furthermore, scripts/app.js can itself require other scripts, which will in turn be loaded asynchronously.

By convention, the entry point script (scripts/app.js in the previous example) will load a configuration object so that RequireJS can be adapted to your own environment and then the real application entry point is loaded.

// - - - - - - -
// scripts/app.js

requirejs.config({
    baseUrl: 'scripts/lib',
    paths: {
        app: '../app'
    }
});

requirejs(['jquery', 'app/player'], function ($, player) {
    // ...
});

In the previous example, we first configure the script loader, then we require two modules—first the jQuery library and then a module named player. The baseUrl option in the configuration block tells RequireJS to load all the scripts from the scripts/lib directory, which is relative to the file that loaded scripts/app.js (in this case, index.html). The path's attribute allows you to create exceptions to that baseUrl, rewriting the path to scripts whose require name (known as the module ID) starts with, in this case, the app string . When we require app/player, RequireJS will load a script, which is relative to index.html, scripts/app/player.js.

Once those two modules are loaded, RequireJS will invoke the callback function that you passed to the requirejs function, adding the modules that were requested as parameters in the same order as specified.

You may be wondering why we talked about both CommonJS and RequireJS since the goal is to share as much code as possible between the server and the client. The reason for covering both methods and tools is for completeness and information purposes only. Since Node.js already uses CommonJS for its module-loading strategy, there is little reason to use RequireJS in the server. Instead of mixing RequireJS for use in the browser, what is commonly done (this will be the approach of choice for the rest of the book) is to use CommonJS for everything (including client-side code) and then run a tool called Browserify over the client code, making it possible to load scripts in the browser that make use of CommonJS. We'll cover Browserify shortly.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.41.229