One thing that Node.js got right from the beginning was to introduce an explicit way of obtaining and using functionality. JavaScript in the browser suffered from the global scope problem, which caused many headaches for developers.
Global scope
In JavaScript, the global scope refers to functionality that is accessible from every script running in the same application. On a website, the global scope is usually the same as the window
variable. Attaching variables to the global scope may be convenient and sometimes even necessary, but it may also lead to conflicts. For instance, two independent functions could both try to write and read from the same variable. The resulting behavior can then be hard to debug and very tricky to resolve. The standard recommendation is to avoid using the global scope as much as possible.
The idea that other functionalities are explicitly imported was certainly not new when Node.js was introduced. While an import mechanism existed in other programming languages or frameworks for quite some time, similar options have also been available for JavaScript in the browser – via third-party libraries such as RequireJS.
Node.js introduced its module system with the name CommonJS. The basis for Node.js’s implementation was actually a project developed at Mozilla. In that project, Mozilla worked on a range of proposals that started with non-browser use but later on expanded to a generic set of JavaScript specifications for a module system.
CommonJS implementations
Besides the implementation in Node.js, many other runtimes or frameworks use CommonJS. As an example, the JavaScript that can be used in the MongoDB database leverages a module system using the CommonJS specifications. The implementation in Node.js is actually only partially fulfilling the full specification.
A module system is crucial for allowing the inclusion of more functionality in a very transparent and explicit manner. In addition to a set of more advanced functionality, a module system gives us the following:
- A way of including more functionality (in CommonJS, via the global
require
function)
- A way of exposing functionality, which can then be included somewhere else (in CommonJS, via the module-specific
module
or exports
variables)
At its core, the way CommonJS works is quite simple. Imagine you have a file called a.js
, which contains the following code:
const b = require('./b.js');
console.log('The value of b is:', b.myValue);
Now the job of Node.js would be to actually make this work, that is, give the b
variable a value that represents the so-called exports of the module. Right now, the script would error out saying that a b.js
file is missing.
The b.js
file, which should be adjacent to a.js
, reads as follows:
exports.myValue = 42;
When Node.js evaluates the file, it will remember the defined exports. In this case, Node.js will know that b.js
is actually just an object with a myValue
key with a value of 42
.
From the perspective of a.js
, the code can therefore be read like this:
const b = {
myValue: 42,
};
console.log('The value of b is:', b.myValue);
The advantage of using the module system is that there is no need to write the outputs of the module again. The call to require
does that for us.
Side effects
Replacing the call to require
with the module’s outputs is only meant for illustrative purposes. In general, this cannot be done as the module evaluation can have some so-called side effects. A side effect happens when implicit or explicit global variables are manipulated. For instance, already writing something to the console or outputting a file in the module evaluation is a side effect. If we’d only replace the require
call with the imported module’s exports, we would not run the side effects, which would miss a crucial aspect of the module.
In the given example, we used the name of the file directly, but importing a module can be more subtle than that. Let’s see a refined version of the code:
a.js
const b = require('./b');
console.log('The value of b is:', b.myValue);
The call to./b.js
has been replaced by ./b
. This will still work, as Node.js will try various combinations for the given import. Not only will it append certain known extensions (such as .js
) but it will also look at whether b
is actually a directory with an index.js
file.
Therefore, with the preceding code, we could actually move b.js
from a file adjacent to a.js
to an index.js
file in the adjacent directory, b
.
The greatest advantage, however, is that this syntax also allows us to import functionality from third-party packages. As we will explore later in Chapter 2, Dividing Code into Modules and Packages, our code has to be divided into different modules and packages. A package contains a set of reusable modules.
Node.js already comes with a set of packages that don’t even need to be installed. Let’s see a simple example:
host.js
const os = require('os');
console.log('The current hostname is:', os.hostname());
The preceding example uses the integrated os
package to obtain the current computer’s network name.
We can run this script with node
in the command line:
$ node host.js
The current hostname is: DESKTOP-3JMIDHE
This script works on every computer that has Node.js installed.