NodeJS Module Types :
NodeJS has three module types :
- Core Modules : These are built-in modules that are part of Node.js distribution and are availabe globally without needing to install any additional distribution. Example : fs, http, etc.
- Local Modules : Custom modules written by developers and saved as individual files.
- Third-Party Modules : Written by external developers, installed and managed by Node Package Manager (NPM).
NodeJS Module Systems :
Node provides us with 2 module system to work with modules :
- CommonJS : It is the original way to work with modules using the
require()
syntax to import andmodule.exports
to export modules. - ESM (ECMAScript modules): Newer module system introduced in es6. It uses
import
to import modules andexport
to export modules.
Module Importing Priority : core modules > third party modules (node_modules) > local modules
CommonJS :
Importing :
In CommonJS syntax, we import using the require()
function. When the require function is invoked, Node goes through the following steps :
- Resolving
- Loading
- Wrapping : Code inside the module is wrapped in a special wrapper which gives the encapsulation and provides with some special properties :
(function (exports, require, module, __filename, __dirname) {
// Module code lives in here
});
- Evaluating : Node executes the code within the wrapeer and exports function and variables mentioned in module.exports
- Caching : Modules are cached after loading for the first time. So, any later loading of the module simply returns the cached value without executing it again. Caching will be discussed in details in a later section.
By wrapping the code in this wrapper, Node.js achieves a few things :
- It keeps top-level variables (defined with
var
,const
, orlet
) scoped to the module rather than the global object. - It helps to provide some global-looking variables that are actually specific to the module, such as:
__filename
and__dirname
Exporting :
In CommonJS, we can export variables or functions with the export
property of the module
object provided by the wrapper.
- For default export, we can set our
module.export
directly to whatever we want to export. Also, In the importing file, we don't need to match the name of the exported variable/function, as we can have only one default export per module.
// my-module.js
function hello(name) {
console.log(`Hello, ${name}!`);
}
module.exports = hello;
// app.js
const whateverYouNameIt = require("./my-module");
hello("world");
Logging the module object in my-module.js
gives us:
Module {
id: '.',
path: '/home/user/Lessons/nodejs',
exports: [Function: hello],
filename: '/home/user/Lessons/nodejs/module.js',
loaded: false,
children: [],
paths: [
...
]
}
- For named export, we can bind all our exportable variables/functions to the
module.exports
object. Then we can destructure to use specific ones.
// my-module.js
function hello(name) {
console.log(`Hello, ${name}!`);
}
module.exports = {
hello,
};
or,
// my-module.js
module.exports.hello = function (name) {
console.log(name);
};
// app.js
const { hello } = require("./my-module");
hello("world");
Logging the module object in my-module.js
gives us:
Module {
id: '.',
path: '/home/user/Lessons/nodejs',
exports: { hello: [Function: hello] },
filename: '/home/user/Lessons/nodejs/module.js',
loaded: false,
children: [],
paths: [
...
]
}
exports vs module.exports :
From the wrapper, we get a exports
object, this is just a reference to the module.exports
object. So, whatever we add to the export
object, gets added to the module.exports
as well. But if we reassign export
with some variable/function in hope of default exporting, the link is broken, and module.exports
will return an empty object.
ECMAScript Modules :
Importing :
To use import syntax, we need to set "type" : "module"
in package.json
.
Different than CommonJS module, in ESM module,
import {myFunction} from "./circle.js"
and,
import {myFunction} from "./circle"
isn't the same.
because, in ESM we can import other type of modules beside JS files like JSON or CSS files, and specifying the extension is necessary to disambiguate between them.
When import is invoked, Node goes through the following steps :
- Resolving : The resolving algorithm is different than that of CommonJS.
- Loading
- Wrapping : There is no wrapper for ES modules. they are native to the underlying JS engine. Specifically, ECMAScript modules have their own module scope, which means that the variables and functions defined in an ECMAScript module are not global.
- Evaluating: Node executes the code within the module.
- Caching: Modules are cached after loading for the first time. So, any later loading of the module simply returns the cached value without executing it again.
Exporting :
In ESM, we have export
for named exports and export default
for default exports.
// my-module.js
export function hello(name) {
console.log(`Hello, ${name}!`);
}
export default function logger(message) {
console.log(message);
}
// app.js
import whateverYouNameIt, { hello } from "./my-module.js";
whateverYouNameIt("custom message"); // calls the logger function, we can name it anything because of it being a default export
hello("world");
We can have multiple exports from a module, but only one default export.
Module caching :
Caching :
When loading a module, NodeJS executes the code once and caches the result. On subsequent imports from same or other modules, it returns the cached results.
Caching example in CommonJS :
Let's do a little fun experiment to see this in action.
In one.js
:
// We generate the current timestamp and add 5 to it
// so, each time we execute this following code, we will have different results
console.log("one.js execution started");
let timestamp = Date.now();
let number = 5;
let sum = timestamp + number;
module.exports = {
sum,
};
In two.js
:
// This file imports the time dependant sum
// but executes after a 5 seconds delay
// We export a dummy variable from this file to reference in three.js
console.log("two.js execution started");
console.log("Loop started");
const startTime = Date.now();
while (Date.now() < startTime + 5000) {}
console.log("Loop finished after 5 seconds");
const { sum } = require("./one");
console.log(`Sum within two.js is ${sum}`);
let dummy = 8;
module.exports = {
dummy,
};
In three.js
:
// This file imports both one.js and two.js
console.log("three.js execution started");
const { sum } = require("./one");
console.log(`Sum within three.js is ${sum}`);
// two.js is imported after getting the sum from one.js
const { dummy } = require("./two");
Intuitively, when we run three.js
with node three.js
, we expect it:
- Log "three.js execution started"
- Import the
one.js
module and execute its code - Log "Sum within three.js is (current timestamp + 5)" with the current time of execution.
- Importing
two.js
module starts the 5 seconds loop and then requires theone.js
file to get the sum. So, technically, timestamp is in seconds, so for five seconds delay, we can expect some difference - The log within
two.js
should generate a sum greater than the one logged inthree.js
due to difference in execution time.
So, log output should be something like this :
three.js execution started
one.js execution started
Sum within three.js is 1683184022476
two.js execution started
Loop started
Loop finished after 5 seconds
Sum within two.js is 1683184022481
But, what we really get is this :
three.js execution started
one.js execution started
Sum within three.js is 1683184022476
two.js execution started
Loop started
Loop finished after 5 seconds
Sum within two.js is 1683184022476
Even with the five seconds loop, we get the same value for sum which is dependant on time !
So, by now we should be clear that NodeJS when importing a module for the first time, executes the code once and caches the result for future imports.
Caching example in ESM :
// my-module.js
let count = 0;
console.log("Module loaded");
export function increment() {
count++;
}
export function getCount() {
return count;
}
// app.js
import { increment, getCount } from "./my-module.js";
console.log("Import 1");
increment();
console.log(getCount()); // 1
console.log("Import 2");
increment();
console.log(getCount()); // 2
//some-other-file.js
// Import the module again from a different file
import { getCount } from "./my-module.js";
console.log(getCount()); // 2 (the same value as the previous call to getCount())
References used :
More details on the underlying workings can be found at NodeJS specification :
Top comments (0)