Node.js Module System
To allow Node.js files to call each other, Node.js provides a simple module system.
Modules are the fundamental components of Node.js applications, and there is a one-to-one correspondence between files and modules. In other words, a Node.js file is a module, which could be JavaScript code, JSON, or compiled C/C++ extensions.
Importing Modules
In Node.js, importing a module is straightforward. Below, we create a main.js file and import the hello module with the following code:
var hello = require('./hello');
hello.world();
In the example above, the code require('./hello')
imports the hello.js file from the current directory (./***
indicates the current directory, and Node.js defaults to the js extension).
Node.js provides two objects, exports
and require
. exports
is the interface exposed by the module, and require
is used to obtain the interface of an external module, i.e., the exports
object of the module.
Next, we create the hello.js file with the following code:
exports.world = function() {
console.log('Hello World');
}
In the example above, hello.js exposes the world
function via the exports
object. In main.js, the module is loaded using require('./hello')
, allowing direct access to the member functions of the exports
object in hello.js.
Sometimes, we want to encapsulate an object within a module, as follows:
module.exports = function() {
// ...
}
For example:
// hello.js
function Hello() {
var name;
this.setName = function(thyName) {
name = thyName;
};
this.sayHello = function() {
console.log('Hello ' + name);
};
};
module.exports = Hello;
This allows direct access to the object:
// main.js
var Hello = require('./hello');
hello = new Hello();
hello.setName('BYVoid');
hello.sayHello();
The only change in the module interface is using module.exports = Hello
instead of exports.world = function(){}
. When the module is referenced externally, its interface is the Hello
object itself, not the original exports
.
Where to Place Server-side Modules
You may have noticed that we have already used modules in our code. For example:
var http = require("http");
...
http.createServer(...);
Node.js comes with an http module, which we request in our code and assign the returned value to a local variable.
This makes our local variable an object with all the public methods provided by the http module.
The file lookup strategy in Node.js's require
method is as follows:
Since Node.js has four types of modules (native modules and three types of file modules), despite the simplicity of the require
method, the internal loading is quite complex, with different loading priorities. As shown in the diagram:
Loading from File Module Cache
Although native modules and file modules have different priorities, they both prefer to load from the file module cache if the module already exists.
Loading from Native Modules
The priority of native modules is just below that of the file module cache. After resolving the filename, the require
method checks whether the module is in the list of native modules. For example, with the http module, even if there is an http/http.js/http.node/http.json file in the directory, require("http")
will not load from these files but from the native module.
Native modules also have a cache area, and they are loaded from the cache area first. If the cache area has not been loaded, the native module's loading method is used for loading and execution.
Loading from Files
When a file module is not in the cache and is not a native module, Node.js resolves the parameter passed to the require
method and loads the actual file from the file system. The details of the loading process, including packaging and compilation, have been described previously. Here, we will describe the process of finding file modules, with some details worth knowing.
The require
method accepts the following types of parameters:
- http, fs, path, etc., native modules.
- ./mod or ../mod, file modules with relative paths.
/pathtomodule/mod
, a file module with an absolute path.mod
, a file module that is not a native module.
Execution order of require(X)
statement under path Y:
1. If X is a built-in module
a. Return the built-in module
b. Stop execution
2. If X starts with '/'
a. Set Y to the root file path
3. If X starts with './' or '/' or '../'
a. LOAD_AS_FILE(Y + X)
b. LOAD_AS_DIRECTORY(Y + X)
4. LOAD_NODE_MODULES(X, dirname(Y))
5. Throw an exception "not found"
LOAD_AS_FILE(X)
1. If X is a file, load X as JavaScript text and stop execution.
2. If X.js is a file, load X.js as JavaScript text and stop execution.
3. If X.json is a file, parse X.json as a JavaScript object and stop execution.
4. If X.node is a file, load X.node as a binary addon and stop execution.
LOAD_INDEX(X)
1. If X/index.js is a file, load X/index.js as JavaScript text and stop execution.
2. If X/index.json is a file, parse X/index.json as a JavaScript object and stop execution.
3. If X/index.node is a file, load X/index.node as a binary addon and stop execution.
LOAD_AS_DIRECTORY(X)
1. If X/package.json is a file,
a. Parse X/package.json, and find the "main" field.
b. let M = X + (json main field)
c. LOAD_AS_FILE(M)
d. LOAD_INDEX(M)
2. LOAD_INDEX(X)
LOAD_NODE_MODULES(X, START)
1. let DIRS=NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
a. LOAD_AS_FILE(DIR/X)
b. LOAD_AS_DIRECTORY(DIR/X)
NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let I = count of PARTS - 1
3. let DIRS = []
4. while I >= 0,
a. if PARTS[I] = "node_modules" CONTINUE
b. DIR = path join(PARTS[0 .. I] + "node_modules")
c. DIRS = DIRS + DIR
d. let I = I - 1
5. return DIRS
Usage of exports and module.exports
If you want to expose properties or methods, use exports. If you want to expose an object (similar to a class, containing many properties and methods), use module.exports.