Open CheatSheet Back To List Next Tutorial

Modules

Intro

  • This tutorial covers JavaScript Modules.

  • It is recommended that you follow along with each section below. Paste or type the code into your own JavaScript and HTML files. 

  • The example code includes modules in both the Node.js and Browser environments. The javascript-tutorial Readme file has instructions on how to run the code including in the Browser environment.


1. Modules Overview

  • In JavaScript, modules are individual files that contain related code that can be imported into other JavaScript files when needed.

  • Back when JavaScript was only run in the browser, there would generally be a single JavaScript file attached to an HTML document through the script tag, and modules were not needed. And like today, JavaScript built-in objects and Web APIs were all attached to the window Global Object.
  • But then in 2009 Node.js was released allowing JavaScript to be run outside the browser on servers and personal computers.

  • Node.js is based on the use of modules, including the Node core modules, third party packages installed in the node_modules directory, and the files created by the user for the project. All of these are separate modules.

  • When Node was released, official JavaScript, know as ECMAScript, did not offer module functionality, so Node adopted a project called CommonJS that created standards for working with modules outside the browser. The CommonJS project was started by a Mozilla engineer, and is not part of the ECMAScript standard.

  • It was not until ES6 was released in 2015 that Module functionality was added to ECMAScript with the addition of import and export statements. 

  • But even then, Node did not adopt the Import/Export functionality until version 13 was released in 2019.
  • Currently Node projects can use either CommonJS or Import/Export syntax.
  • In this tutorial we will cover both.

  • To follow along with this tutorial, create a folder called modules. 


CommonJS

2a. CommonJS: Import Node modules or packages

  • We will start with CommonJS syntax, which uses the require function to import modules.
  • Place the require statements at the top of the file.
  • Node.js has several core built-in modules. Some are attached to the global object, others need to be imported before being used.
  • Create another folder in the modules folder called commonjs and add a file called app.js. Populate it with the below:
modules/common.js/app.js
const path = require('path');
const { existsSync, readFileSync } = require('fs');
const filePath = path.join('.', 'modules', 'commonjs', 'calculator.js'); filePath; // returns "modules/commonjs/calculator.js"
if (existsSync(filePath)) {
  console.log(readFileSync(filePath, 'utf8'));
} else { console.log('File does not exist.'); }

Import a core Node.js module:
  • Some built-in Node.js modules are already attached to the global object and don't need to be imported to be used. Others are part of the Node.js program, but do need to be imported before being used.
  • The path module is one of those. It has methods for parsing and creating file paths.
  • To use it you need to call the require function, then assign it to a variable. You can name the variable anything you want, but it makes sense to just use the module name: path.
  • To use methods attached to the module, use the path variable as the namespace, and chain the method to it. The path.join method returns a file path string by joining the argument values into one path.
  • const filePath = path.join('.', 'modules', 'commonjs', 'calculator.js') returns: "modules/commonjs/calculator.js"

Import specific methods from a core Node.js module: 
  • You can import just the methods you are going to use with destructuring variable assignment. 
  • At the top of the app file we import the existsSync and readFileSync methods from the File System module fs:
    • const { existsSync, readFileSync } = require('fs');
  • Then we can call the methods without having to use the module name as namespace. 
if (existsSync(filePath)) {
  console.log(readFileSync(filePath, 'utf8'));
}

  • The existsSync method checks if the file at the given path exists. If so we call the readFileSync method to read the file content and use console.log to log the file content to the terminal. 
  • Console, path and fs are all node modules. The console module differs from path and fs in that it is attached to the global object so you don't have to import it before using it. 

  • Run the app file with the Debugger or with node: node modules/common.js/app.js
  • The calculator.js file doesn't exist yet so it won't log anything. We will add that next. Then you can run the app.js file again, and the contents of the calculator.js file will be logged to the console. 

  • These were examples of using core Node.js modules. The same idea holds for importing and using third party Node.js packages. It also holds for user created modules except you also have to export the code before you can import it.


2b. CommonJS: User created modules - single export

  • In a Node project you will almost always create multiple files. Each file is a module. 
  • Add a file to the commonjs folder named calculator.js, and populate it with the below. 
modules/common.js/calculator.js
const calculator = {
  double: (x) => x * 2,
  triple: (x) => x * 3,
};
module.exports = calculator;

  • The file holds a single object called calculator with methods to double and triple a number. 
  • You export a single construct like this object using the module.exports property assigned to the object begin exported: module.exports = calculator;
  • Put that at the bottom of the file.

  • Then import the module in the app.js file and use it in your code. 
modules/common.js/app.js
...
const calculator = require('./calculator');
...
const res1 = calculator.double(10);
const res2 = calculator.triple(10); console.log(res1, res2);

  • In the app.js file import the calculator module with the require function:
    • const calculator = require('./calculator');
    • You must include the path to the file. "./" is a relative path to the current directory. In fact using "./" is optional, but including it makes it clear that it refers to a file, not an npm package.
    • You can leave the .js extension off the file name. If there is no extension after the file name, js is assumed.
    • Set the result of the require function to a variable. It can be any variable name but it is generally clearer if you use the file name.

  • To use the methods from the module, chain them to the namespace like this: calculator.double(10)

  • Instead of importing the whole module, you can use destructuring variable assignment to create variables for the double and triple methods.
  • Then you can call the functions without the calculator namespace.
modules/common.js/app.js
...
const { double, triple } = require('./calculator');
...
const res3 = double(10);
const res4 = triple(10); console.log(res3, res4);


2c. CommonJS: User created modules - multiple exports

  • A module file can contain multiple functions or other constructs. Then you export them as properties of a module.exports object. And import them using destructuring variable assignment.

  • Create another module called calculator2.js that exports two separate functions. The half function divides the parameter value by two and the third function divides the parameter value by three. 
  • To export them, set module.exports to an object containing properties for those two functions.

modules/common.js/calculator2.js
const half = (x) => x / 2;
const third = (x) => x / 3;

module.exports = { half, third };


  • Back in the app.js file call the require function to import the module and use destructuring variable assignment to get the individual functions. 
  • Then call the half and third functions passing in 10 as the argument.
modules/common.js/app.js
...
const { half, third } = require('./calculator2');
...
const res5 = half(10); const res6 = third(10); console.log(res5, res6); // logs 5, 3.333333333


Import/Export statements

ECMAScript module functionality

  • Browsers don't support CommonJS. 
  • ECMAScript (i.e., official JavaScript) introduced module functionality with ES6 released in 2015. It requires the use of import and export statements.
  • Browsers did not support ECMAScript module functionality natively until 2017 (Safari, Chrome, Edge, Opera) and May 2018 (FireFox). Node.js did not fully support it until November 2019 with version 13.2.

  • JavaScript engines treat module files a little differently from regular script files (ref v8.dev/features/modules). Some of these differences include:
    • Modules have strict mode enabled by default.
    • In regular script files, variables declared with var and function declarations at the top level are attached to the global object (window in the browser, global in Node.js). But in modules they are scoped to the module unless explicitly attached to the global object (e.g., window.myVar = 'some value'). Modules can access globally defined variables however. 
    • Modules allow top-level await, script files do not. Top level await allows you to use the await keyword at the top level of a module without having to be inside an async function.
    • The import and export statements are only available to modules. 

  • Because of these differences in the way modules are handled, the JavaScript engine needs to know which scripts are modules at runtime. 

  • In Node.js there are two ways to do this:
    1. Use .mjs extensions on the module filenames.
    2. Or the nearest parent package.json file must contain "type": "module"

  • In the browser set the type attribute to "module" in the script tag: <script type="module" src="app.js"></script>

Other differences between CommonJS and ECMAScript modules:
  • You cannot use CommonJS and ECMAScript modules at the same time. For instance you can't use the export statement to export a module and then use require to import it. You have to use one or the other (or neither).
  • CommonJS has convenience variables called __dirname and __filename that get the current path to the directory or file. These variables are not available in ECMAScript modules.
  • CommonJS allows you to leave out the .js file extension when importing files. In ECMAScript modules the .js or .mjs extension is required when importing your files.

3a. Import modules with the import statement

  • Once your files have been identified as modules they can export their content with the export statement, and their content can be imported with with the import statement.

  • Import statement syntax when importing a default export:
    • import variableName from 'moduleName'; 
  • Import statement syntax when importing one or more named exports:
    • import { export1[, export2, ...] } from 'moduleName'; 

  • We will first demonstrate how to use import/export statements with Node.js. Node uses CommonJS modules by default. We need to let Node know if we are using ECMAScript modules. One way is to use .mjs file extensions.
  • In the modules folder create a folder called mjs-examples, then add a file to it called app.mjs and populate it with the below:

modules/mjs-examples/app.mjs
import path from 'path';
import { existsSync, readFileSync } from 'fs';
import calculator from './calculator.mjs';
import { half, third } from './calculator2.mjs';
const filePath = path.join('.', 'modules', 'mjs', 'calculator.mjs');
if (existsSync(filePath)) {
  console.log(readFileSync(filePath, 'utf8'));
}
const res1 = calculator.double(10);
const res2 = calculator.triple(10);
const res3 = half(10);
const res4 = third(10);
console.log(res1, res2, res3, res4);

  • This file is like the app.js file we used earlier, but it uses import statements instead of CommonJS require functions. And our module names use extension .mjs. 
  • The path and calculator.mjs module variable is a single namespace for the default export.
  • The fs and calculator2.mjs modules use destructuring variable assignment to specific named exports.

3b. Export modules with the export statement - default export
  • When you split your code into multiple files you need to export the content of the module files. If the file contains a single object or other construct, you can export it with export default then the variable name:
    • export default varName; 

  • Create a file called calculator.mjs and populate it with a single object named calculator that contains two methods named double and triple. Export the object as a default export at the bottom of the file.
modules/mjs-examples/calculator.mjs
const calculator = {
  double: (x) => x * 2,
  triple: (x) => x * 3,
};
export default calculator;

3c. Export modules with the export statement - multiple named exports
  • If your module contains multiple functions or other constructs, you can export them with the export statement followed by curly braces containing the function or variable names. These are called "named exports"
    • export { var1[, var2,...] };
  • In the importing file, use destructuring variable assignment to import the specific named exports needed from the module. 
  • The named export names can then be used without the module namespace.

  • Create a file named calculator2.mjs and populate it with two functions: half and double:
modules/mjs-examples/calculator2.mjs
const half = (x) => x / 2;
const third = (x) => x / 3;

export { half, third };

  • To export the functions as named exports, place them at the bottom of the file in curly braces after the export keyword.

  • Now you can run the module with the Debug sidebar or from the Terminal with node: 
node modules/mjs-examples/app.mjs

  • You should see the results logged to the console: 20 30 5 3.3333333333333335


3d. Rename imports

  • In this section we will demonstrate how to rename imports.

  • When importing a module that has just a single default export, you can give it whatever variable name you want. For example, you can import the calculator.mjs module and instead of using the variable name calculator you can instead call it multiplier.
    • import multiplier from './calculator.mjs';

  • Then you would chain the module's methods to the multiplier variable: multiplier.double(10)

  • Our calculator2.mjs module has two named exports: half and third.

  • Create a namespace for named exports: To create a namespace when importing a module with multiple named exports, import it with a wildcard * and assign it an alias.
    • import * as name from 'module'; 
  • Rename named exports: You can rename specific named exports as part of destructuring variable assignment.
    • import { var1 as alias1[, var2 as alias2,...] } from 'module'; 

  • We will demonstrate both of these. Create a file named app-aliases.mjs and populate it with the below.
modules/mjs-examples/app-aliases.mjs
import * as divider from './calculator2.mjs';
import { half as by2, third as by3 } from './calculator2.mjs';

const res1 = divider.half(10);
const res2 = divider.third(10);

const res3 = by2(10);
const res4 = by3(10);

console.log(res1, res2, res3, res4);

  • At the top of the file we are importing the calculator2.mjs module's named exports and assigning them to a namespace called divider
    • import * as divider from './calculator2.mjs';
    • We call the functions using the namespace with the function name: 
const res1 = divider.half(10)
const res2 = divider.third(10) 

  • We are also importing calculator2 using destructuring and assigning aliases of by2 and by3 to the half and third functions.
    • import { half as by2, third as by3 } from './calculator2.mjs';
    • We then call the function with the alias:
const res3 = by2(10);
const res4 = by3(10);

  • Run it with the Debugger or from the Terminal: node modules/mjs-examples/app-aliases.mjs 
  • It should log the results: 5 3.3333333333333335 5 3.3333333333333335


3e. Import/Export statements: Node - set type to module

  • The other way to let Node know we are using ECMAScript modules is to set the "type" to "module" in the nearest parent package.json file.

  • Make a folder called module-type, add an app.js file, with the js extension to it. Populated it with the below:
modules/module-type/app.js
import calculator from './calculator.js';

const res1 = calculator.double(10);
const res2 = calculator.triple(10);
console.log('Log results from calculator module:', res1, res2);

  • Add a calculator.js file with a js extension:
modules/module-type/calculator.js
const calculator = {
  double: (x) => x * 2,
  triple: (x) => x * 3,
};

export default calculator;

  • Try to run it with the Debugger or from the Terminal: node modules/module-type/app.js
  • You will get an error saying: "SyntaxError: Cannot use import statement outside a module"

  • Node.js projects generally have a file in the project root directory called package.json to manage third party Node.js packages and add meta info about your app. 

  • Create the package.json and populate it with:
modules/module-type/package.json
{
  "main": "app.js",
  "type": "module"
}

  • This tells Node.js that the project's main file is app.js and its file type is module. Now all the project's files will be treated as modules, and you can use import/export statements instead of the CommonJS require function.

  • Now try to run the app again with the Debugger or from the Terminal: node modules/module-type/app.js 
  • It should print: "Log results from calculator module: 20 30"


3f. Import/Export statements: Browser - set type to module

  • Lastly, we have only been using Node.js examples so far. Let's see how to use modules with the browser.
  • Browsers do not recognize the CommonJS require function, so you can only use the import/export statements.

  • Duplicate the module-type folder and name the new folder browser.
  • Delete the package.json file, leaving only the app.js and calculator.js files.

  • Add a very simple index.html file.
modules/browser/index.html
<!DOCTYPE html>
<html lang="en">
<body>
  <h1>JavaScript Modules</h1>
<script type="module" src="app.js"></script>
</body>
</html>

  • At the bottom of the file is a script tag that will run JavaScript in the Browser when the web page opens. 
    • <script type="module" src="app.js" ></script>
    • The src source attribute is set to the app.js file. 
    • Setting the type attribute to "module" will tell the browser to treat the JavaScript files as modules. 

  • To test it we need to launch a local web server. You can use the VS Code text editor and the LiveServer extension. 
  • If the index.html file was in the project's root directory we would be ready to launch. But since it is not we need to tell Live Server what file to launch.
  • Open the settings.json file in the javascript-tutorial project's root directory, or create one if it doesn't exist. Then change the liveServer.settings.root property to path: "/modules/browser"
settings.json
{
  "liveServer.settings.port": 5500,
  "liveServer.settings.root": "/modules/browser"
}

  • Now we are ready to launch the server. Open the index.html file in the text editor, right click anywhere on the index.html page, then select Open With LiveServer.
  • It should then automatically open the web page in your browser at URL: http://127.0.0.1:5500/

  • The web page will just display the heading "JavaScript Modules".
  • The calculator module just logs the results of the method calls to the console. But not to the Terminal's console because we are not running our app with Node.js. We are running it with the Browser. We need to open the browser console using Chrome Developer Tools.
  • Right click on the open web page and select Inspect. Then click the console tab. And you should see: "Log results from calculator module: 20 30"

  • And that concludes this tutorial on JavaScript modules.


Conclusion

The topics in this tutorial correspond with the JavaScript CheatSheet Modules category. Make sure you understand each topic so you can refer back to the CheatSheet when working on your own projects.

If you have the CheatSheet desktop app and downloaded the JavaScript CheatSheet, then go through the flashcards for this category to the point where you can answer them all in order and shuffled.
Open CheatSheet Back To List Next Tutorial